Compare commits

...

20 Commits

Author SHA1 Message Date
3c240fc16b Solved bug 2025-01-09 16:34:35 +01:00
2a3491b98b Added warnings 2025-01-09 16:30:38 +01:00
378ee4632f Finish modals 2025-01-09 15:57:39 +01:00
19f99ff9d3 Optimized description 2025-01-09 15:46:45 +01:00
a9fcd4b6de Solved close modals bug 2025-01-09 15:38:48 +01:00
e303968ca5 Solved bugs 2025-01-09 15:34:32 +01:00
f85dc5bb18 Removed allerts 2025-01-09 15:19:37 +01:00
562f5989e1 Solved identifier bug 2025-01-09 15:17:34 +01:00
9455f40079 Optimized modals 2025-01-09 14:59:30 +01:00
d59cc73470 Implemented hover submenüs 2025-01-09 14:42:38 +01:00
7a66184a46 Added dynamic submenus 2025-01-09 14:36:44 +01:00
d8ec067675 Refactored App.py 2025-01-09 14:27:07 +01:00
c87c1df10a Finished implementation of configuration resolver 2025-01-09 14:20:59 +01:00
8fb0cecfbe Added detailled debug infos 2025-01-09 13:56:29 +01:00
61af45e837 Added conf resv base 2025-01-09 13:53:56 +01:00
4ee5340dd3 Optimized config and .gitignore 2025-01-09 13:19:54 +01:00
14ccedf1c1 modified icon function 2025-01-09 12:20:57 +01:00
8959f4405b Removed pictures 2025-01-09 12:03:34 +01:00
e45bd16631 Optimized caching and changed from json to yaml 2025-01-09 11:59:23 +01:00
9b763cd34b Optimized navigation 2025-01-09 09:48:56 +01:00
17 changed files with 920 additions and 550 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
app/static/logos/*
app/static/cache/*
*__pycache__*

View File

@ -1,9 +1,35 @@
# homepage.veen.world
# Landingpage
docker build -t flask-app .
docker run -d -p 5000:5000 --name landingpage flask-app
## Access
http://127.0.0.1:5000
sudo docker run -d -p 5000:5000 --name landingpage -v $(pwd)/app/:/app -e FLASK_APP=app.py -e FLASK_ENV=development flask-app
## Administrate Docker
### Stop and Destroy
```bash
docker stop landingpage
docker rm landingpage
```
### Build
```bash
docker build -t application-landingpage .
```
### Run
#### Development
```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
```
#### Production
```bash
docker run -d -p 5000:5000 --name landingpage application-landingpage
```
### Debug
```bash
docker logs -f landingpage
```

View File

@ -1,62 +1,54 @@
import json
import os
from flask import Flask, render_template
import requests
import hashlib
import yaml
from utils.configuration_resolver import ConfigurationResolver
from pprint import pprint
from utils.cache_manager import CacheManager
def cache_icon(icon_url, cache_dir="static/logos"):
"""Lädt ein Icon herunter und speichert es lokal, wenn es nicht existiert. Fügt einen Hash hinzu."""
# Erstelle das Verzeichnis, falls es nicht existiert
os.makedirs(cache_dir, exist_ok=True)
# Generiere einen 8-Zeichen-Hash basierend auf der URL
hash_object = hashlib.blake2s(icon_url.encode('utf-8'), digest_size=8)
hash_suffix = hash_object.hexdigest()
# Erstelle den Dateinamen mit Hash
base_name = icon_url.split("/")[-2]
filename = f"{base_name}_{hash_suffix}.png"
full_path = os.path.join(cache_dir, filename)
# Wenn die Datei existiert, überspringe den Download
if os.path.exists(full_path):
return full_path
# Initialize the CacheManager
cache_manager = CacheManager()
# Lade die Datei herunter
response = requests.get(icon_url, stream=True)
if response.status_code == 200:
with open(full_path, "wb") as f:
for chunk in response.iter_content(1024):
f.write(chunk)
return full_path
# Clear cache on startup
cache_manager.clear_cache()
def load_config(app):
"""Load and resolve the configuration."""
# Lade die Konfigurationsdatei
with open("config.yaml", "r") as f:
config = yaml.safe_load(f)
# Resolve links in the configuration
resolver = ConfigurationResolver(config)
resolver.resolve_links()
# Update the app configuration
app.config.update(resolver.get_config())
app = Flask(__name__)
load_config(app)
# Hole die Umgebungsvariable FLASK_ENV oder setze einen Standardwert
FLASK_ENV = os.getenv("FLASK_ENV", "production")
config_data = None # Globale Variable für die Konfiguration
def load_config():
"""Lädt die Konfiguration aus der JSON-Datei."""
with open("config.json", "r") as config_file:
return json.load(config_file)
@app.before_request
def reload_config_in_dev():
"""Lädt die Datei bei jedem Request neu im Dev-Modus."""
global config_data
if FLASK_ENV == "development" or config_data is None:
config_data = load_config()
if FLASK_ENV == "development":
load_config(app)
print("DEVELOPMENT ENVIRONMENT")
else:
print("PRODUCTIVE ENVIRONMENT")
# Cachen der Icons
for card in config_data["cards"]:
card["icon"] = cache_icon(card["icon"])
# Cache the icons
for card in app.config["cards"]:
card["icon"]["cache"] = cache_manager.cache_file(card["icon"]["source"])
app.config["company"]["logo"]["cache"] = cache_manager.cache_file(app.config["company"]["logo"]["source"])
app.config["company"]["favicon"]["cache"] = cache_manager.cache_file(app.config["company"]["favicon"]["source"])
@app.route('/')
def index():
return render_template("pages/index.html.j2", cards=config_data.get("cards", []), networks=config_data.get("networks", []), company=config_data["company"], navigation=config_data["navigation"])
return render_template("pages/index.html.j2", cards=app.config["cards"], company=app.config["company"], navigation=app.config["navigation"])
if __name__ == "__main__":
app.run(debug=(FLASK_ENV == "development"), host="0.0.0.0", port=5000)
app.run(debug=(FLASK_ENV == "development"), host="0.0.0.0", port=5000)

View File

@ -1,459 +0,0 @@
{
"cards": [
{
"icon": "https://cloud.veen.world/s/logo_agile_coach_512x512/download",
"title": "Agile Coach",
"text": "I lead agile transformations and improve team dynamics through Scrum, DevOps, and Agile Coaching. My goal is to enhance collaboration and efficiency in organizations, ensuring agile principles are effectively implemented for sustainable success.",
"link": "https://www.agile-coach.world",
"link_text": "www.agile-coach.world"
},
{
"icon": "https://cloud.veen.world/s/logo_personal_coach_512x512/download",
"title": "Personal Coach",
"text": "Offering personalized coaching for growth and development, I utilize a blend of hypnotherapy, mediation, and holistic techniques. My approach is tailored to help you achieve personal and professional milestones, fostering holistic well-being.",
"link": "https://www.personalcoach.berlin",
"link_text": "www.personalcoach.berlin"
},
{
"icon": "https://cloud.veen.world/s/logo_yachtmaster_512x512/download",
"title": "Yachtmaster",
"text": "As a Yachtmaster, I provide comprehensive sailing education, yacht delivery, and voyage planning services. Whether you're learning to sail or need an experienced skipper, my expertise ensures a safe and enjoyable experience on the water.",
"link": "https://www.yachtmaster.world",
"link_text": "www.yachtmaster.world"
},
{
"icon": "https://cloud.veen.world/s/logo_polymath_512x512/download",
"title": "Polymath",
"text": "I support the evaluation and execution of complex cross-domain projects, offering insights across land, sea, sky, and digital realms. My expertise helps clients navigate and succeed in multifaceted environments with strategic precision.",
"link": "https://www.crossdomain.consulting/",
"link_text": "www.crossdomain.consulting"
},
{
"icon": "https://cloud.veen.world/s/logo_cybermaster_512x512/download",
"title": "Cybermaster",
"text": "Specializing in open-source IT solutions for German SMBs, I focus on automation, security, and reliability. My services are designed to create robust infrastructures that streamline operations and safeguard digital assets.",
"link": "https://www.cybermaster.space",
"link_text": "www.cybermaster.space"
},
{
"icon": "https://cloud.veen.world/s/logo_prompt_master_512x512/download",
"title": "Prompt Engineer",
"text": "Leveraging AI's power, I specialize in crafting custom prompts and creative content for AI-driven applications. My services are aimed at businesses, creatives, and researchers looking to harness AI technology for innovation, efficiency, and exploring new possibilities.",
"link": "https://promptmaster.nexus",
"link_text": "www.promptmaster.nexus"
},
{
"icon": "https://cloud.veen.world/s/logo_mediator_512x512/download",
"title": "Mediator",
"text": "Specializing in resolving interpersonal and business conflicts with empathy and neutrality, I facilitate open communication to achieve lasting agreements and strengthen relationships. My mediation services are designed for individuals, teams, and organizations to foster a harmonious and productive environment.",
"link": "https://www.mediator.veen.world",
"link_text": "www.mediator.veen.world"
},
{
"icon": "https://cloud.veen.world/s/logo_hypnotherapist_512x512/download",
"title": "Hypnotherapist",
"text": "As a certified Hypnotherapist, I offer tailored sessions to address mental and emotional challenges through hypnosis. My approach helps unlock the subconscious to overcome negative beliefs and stress, empowering you to activate self-healing and embrace positive life changes.",
"link": "https://www.hypno.veen.world",
"link_text": "www.hypno.veen.world"
},
{
"icon": "https://cloud.veen.world/s/logo_skydiver_512x512/download",
"title": "Aerospace Consultant",
"text": "As an Aerospace Consultant with aviation credentials, including a Sport Pilot License for Parachutes, and a Restricted Radiotelephony and Operator's Certificate I deliver expert consulting services. Currently training for my Private Pilot License, I specialize in guiding clients through aviation regulations, safety standards, and operational efficiency.",
"link": null,
"link_text": "Website under construction"
},
{
"icon": "https://cloud.veen.world/s/logo_hunter_512x512/download",
"title": "Wildlife Expert",
"text": "As a certified hunter and wildlife coach, I offer educational programs, nature walks, survival trainings, and photo expeditions, merging ecological knowledge with nature respect. My goal is to foster sustainable conservation and enhance appreciation for the natural world through responsible practices.",
"link": null,
"link_text": "Website under construction"
},
{
"icon": "https://cloud.veen.world/s/logo_diver_512x512/download",
"title": "Master Diver",
"text": "As a certified master diver with trainings in various specialties, I offer diving instruction, underwater photography, and guided dive tours. My experience ensures safe and enriching underwater adventures, highlighting marine conservation and the wonders of aquatic ecosystems.",
"link": null,
"link_text": "Website under construction"
},
{
"icon": "https://cloud.veen.world/s/logo_massage_therapist_512x512/download",
"title": "Massage Therapist",
"text": "Certified in Tantra Massage, I offer unique full-body rituals to awaken senses and harmonize body and mind. My sessions, a blend of ancient Tantra and modern relaxation, focus on energy flow, personal growth, and spiritual awakening.",
"link": null,
"link_text": "Website under construction"
}
],
"company": {
"titel": "Kevin Veen-Birkenbach",
"subtitel": "Consulting and Coaching Solutions",
"logo": "https://cloud.veen.world/s/logo_face_512x512/download",
"address": {
"street": "Afrikanische Straße 43",
"postal_code": "DE-13351",
"city": "Berlin",
"country": "Germany"
},
"imprint_url": "https://s.veen.world/imprint"
},
"navigation": {
"header":[
{
"name": "Microblog",
"description": "Read my microblogs",
"icon_class": "fa-brands fa-mastodon",
"href": "https://microblog.veen.world/@kevinveenbirkenbach",
"subitems": []
},
{
"name": "Pictures",
"description": "View my photo gallery",
"icon_class": "fa-solid fa-camera",
"href": "https://picture.veen.world/kevinveenbirkenbach",
"subitems": []
},
{
"name": "Videos",
"description": "Watch my videos",
"icon_class": "fa-solid fa-video",
"href": "https://video.veen.world/a/kevinveenbirkenbach",
"subitems": []
},
{
"name": "Blog",
"description": "Read my blog",
"icon_class": "fa-solid fa-blog",
"href": "https://blog.veen.world",
"subitems": []
},
{"name": "Code",
"icon_class": "fa-solid fa-laptop-code",
"description": "Check out my Code",
"subitems": [
{
"name": "Github",
"description": "View my GitHub profile",
"icon_class": "bi bi-github",
"href": "https://github.com/kevinveenbirkenbach",
"subitems": []
},
{
"name": "Gitea",
"description": "Explore my code repositories",
"icon_class": "fa-solid fa-code",
"href": "https://git.veen.world/kevinveenbirkenbach",
"subitems": []
}
]
},
{
"name": "Logbooks",
"description": "My activity logs",
"icon_class": "fa-solid fa-book",
"href": null,
"subitems": [
{
"name": "Skydiver",
"description": "View my skydiving logs",
"icon_class": "fa-solid fa-parachute-box",
"href": "https://s.veen.world/skydiverlog",
"subitems": []
},
{
"name": "Skipper",
"description": "See my sailing records",
"icon_class": "fa-solid fa-sailboat",
"href": "https://s.veen.world/meilenbuch",
"subitems": []
},
{
"name": "Diver",
"description": "Check my diving logs",
"icon_class": "fa-solid fa-fish",
"href": "https://s.veen.world/diverlog",
"subitems": []
},
{
"name": "Pilot",
"description": "Review my flight logs",
"icon_class": "fa-solid fa-plane",
"href": "https://s.veen.world/pilotlog",
"subitems": []
},
{
"name": "Nature",
"description": "Explore my nature logs",
"icon_class": "fa-solid fa-tree",
"href": "https://s.veen.world/naturejournal"
}
]
},
{
"name": "Contact",
"description": "Get in touch",
"icon_class": "fa-solid fa-envelope",
"href": null,
"subitems": [
{
"name": "Email",
"description": "Send me an email",
"icon_class": "fa-solid fa-envelope",
"href": "kevin@veen.world",
"href_praefix": "mailto",
"subitems": []
},
{
"name": "Matrix",
"description": "Chat with me on Matrix",
"icon_class": "fa-solid fa-cubes",
"popup": true,
"address": "@kevinveenbirkenbach:veen.world"
},
{
"name": "Mobile",
"description": "Call me",
"icon_class": "fa-solid fa-phone",
"href": "+491781798023",
"href_praefix": "tel",
"subitems": []
},
{
"name": "PGP",
"description": "Access my PGP key",
"icon_class": "fa-solid fa-key",
"href": "https://s.veen.world/pgp",
"subitems": []
},
{
"name": "Signal",
"description": "Message me on Signal",
"icon_class": "fa-brands fa-signal-messenger",
"popup": true,
"href": "+491781798023",
"subitems": []
},
{
"name": "Telegram",
"description": "Message me on Telegram",
"icon_class": "fa-brands fa-telegram",
"target":"_blank",
"href": "https://t.me/kevinveenbirkenbach",
"subitems": []
},
{
"name": "WhatsApp",
"description": "Chat with me on WhatsApp",
"icon_class": "fa-brands fa-whatsapp",
"href": "https://wa.me/491781798023",
"subitems": []
}
]
}
],
"footer":
[
{
"name": "External Accounts",
"description" : "Me on other plattforms",
"icon_class" : "fa-solid fa-external-link-alt",
"subitems":[
{
"name": "Meta",
"description": "Social and developer networks",
"icon_class": "fa-brands fa-meta",
"href": null,
"subitems": [
{
"name": "Instagram",
"description": "Follow me on Instagram",
"icon_class": "fa-brands fa-instagram",
"href": "https://www.instagram.com/kevinveenbirkenbach/",
"subitems": []
},
{
"name": "Facebook",
"description": "Like my Facebook page",
"icon_class": "fa-brands fa-facebook",
"href": "https://www.facebook.com/kevinveenbirkenbach",
"subitems": []
}
]
},
{
"name": "Carreer Profiles",
"icon_class": "fa-solid fa-user-tie",
"subitems": [
{
"name": "XING",
"description": "Visit my XING profile",
"icon_class": "bi bi-building",
"href": "https://www.xing.com/profile/Kevin_VeenBirkenbach",
"subitems": []
},
{
"name": "LinkedIn",
"description": "Connect on LinkedIn",
"icon_class": "bi bi-linkedin",
"href": "https://www.linkedin.com/in/kevinveenbirkenbach",
"subitems": []
}
]
},
{
"name": "Sports",
"description": "My sport activities",
"icon_class": "fa-solid fa-running",
"href": null,
"subitems": [
{
"name": "Garmin",
"description": "My Garmin activities",
"icon_class": "fa-solid fa-person-running",
"href": "https://s.veen.world/garmin",
"subitems": []
},
{
"name": "Eversports",
"description": "My Eversports sessions",
"icon_class": "fa-solid fa-dumbbell",
"href": "https://s.veen.world/eversports",
"subitems": []
}
]
},
{
"name": "Duolingo",
"description": "Learn with me on Duolingo",
"icon_class": "fa-solid fa-language",
"href": "https://www.duolingo.com/profile/kevinbirkenbach",
"subitems": []
},
{
"name": "Spotify",
"description": "Listen to my playlists",
"icon_class": "fa-brands fa-spotify",
"href": "https://open.spotify.com/user/31vebfzbjf3p7oualis76qfpr5ty",
"subitems": []
},
{
"name": "Patreon",
"description": "Support me on Patreon",
"icon_class": "fa-brands fa-patreon",
"href": "https://patreon.com/kevinveenbirkenbach",
"subitems": []
}
]
},
{
"name": "Community",
"description": "My presence in the Fediverse",
"icon_class": "fa-solid fa-users",
"subitems": [
{
"name": "Forum",
"description": "Join the discussion",
"icon_class": "fa-brands fa-discourse",
"href": "https://forum.veen.world/u/kevinveenbirkenbach",
"subitems": []
},
{
"name": "Newsletter",
"description": "Subscribe to my newsletter",
"icon_class": "fa-solid fa-envelope-open-text",
"href": "https://newsletter.veen.world/subscription/form",
"subitems": []
}
]
},
{
"name": "Work Hub",
"description": "Curated collection of self hosted tools for work, organization, and learning.",
"icon_class": "fa-solid fa-toolbox",
"href": null,
"subitems": [
{
"name": "Open Project",
"description": "Explore my projects",
"icon_class": "fa-solid fa-chart-line",
"href": "https://project.veen.world/",
"subitems": []
},
{
"name": "Taiga",
"description": "View my Kanban board",
"icon_class": "bi bi-clipboard2-check-fill",
"href": "https://kanban.veen.world/",
"subitems": []
},
{
"name": "Matomo",
"description": "Analyze with Matomo",
"icon_class": "fa-solid fa-chart-simple",
"href": "https://matomo.veen.world/",
"subitems": []
},
{
"name": "Baserow",
"description": "Organize with Baserow",
"icon_class": "fa-solid fa-table",
"href": "https://baserow.veen.world/",
"subitems": []
},
{
"name": "Elements",
"description": "Chat with me",
"icon_class": "fa-solid fa-comment",
"href": "https://element.veen.world/",
"subitems": []
},
{
"name": "Big Blue Button",
"description": "Join live events",
"icon_class": "fa-solid fa-video",
"href": "https://meet.veen.world/",
"subitems": []
},
{
"name": "Mailu",
"description": "Send me a mail",
"icon_class": "fa-solid fa-envelope",
"href": "https://mail.veen.world/",
"subitems": []
},
{
"name": "Moodel",
"description": "Learn with my academy",
"icon_class": "fa-solid fa-graduation-cap",
"href": "https://academy.veen.world/",
"subitems": []
},
{
"name": "Yourls",
"description": "Find my curated links",
"icon_class": "bi bi-link",
"href": "https://s.veen.world/admin/",
"subitems": []
},
{
"name": "Nextcloud",
"description": "Access my cloud storage",
"icon_class": "fa-solid fa-cloud",
"href": "https://cloud.veen.world/",
"subitems": []
}
]
},
{
"name":"Imprint",
"icon_class":"fa-solid fa-scale-balanced",
"href":"https://s.veen.world/imprint"
}
] }
}

460
app/config.yaml Normal file
View File

@ -0,0 +1,460 @@
---
cards:
- icon:
source: https://cloud.veen.world/s/logo_agile_coach_512x512/download
title: Agile Coach
text: I lead agile transformations and improve team dynamics through Scrum, DevOps,
and Agile Coaching. My goal is to enhance collaboration and efficiency in organizations,
ensuring agile principles are effectively implemented for sustainable success.
url: https://www.agile-coach.world
link_text: www.agile-coach.world
- icon:
source: https://cloud.veen.world/s/logo_personal_coach_512x512/download
title: Personal Coach
text: Offering personalized coaching for growth and development, I utilize a blend
of hypnotherapy, mediation, and holistic techniques. My approach is tailored to
help you achieve personal and professional milestones, fostering holistic well-being.
url: https://www.personalcoach.berlin
link_text: www.personalcoach.berlin
- icon:
source: https://cloud.veen.world/s/logo_yachtmaster_512x512/download
title: Yachtmaster
text: As a Yachtmaster, I provide comprehensive sailing education, yacht delivery,
and voyage planning services. Whether you're learning to sail or need an experienced
skipper, my expertise ensures a safe and enjoyable experience on the water.
url: https://www.yachtmaster.world
link_text: www.yachtmaster.world
- icon:
source: https://cloud.veen.world/s/logo_polymath_512x512/download
title: Polymath
text: I support the evaluation and execution of complex cross-domain projects, offering
insights across land, sea, sky, and digital realms. My expertise helps clients
navigate and succeed in multifaceted environments with strategic precision.
url: https://www.crossdomain.consulting/
link_text: www.crossdomain.consulting
- icon:
source: https://cloud.veen.world/s/logo_cybermaster_512x512/download
title: Cybermaster
text: Specializing in open-source IT solutions for German SMBs, I focus on automation,
security, and reliability. My services are designed to create robust infrastructures
that streamline operations and safeguard digital assets.
url: https://www.cybermaster.space
link_text: www.cybermaster.space
- icon:
source: https://cloud.veen.world/s/logo_prompt_master_512x512/download
title: Prompt Engineer
text: Leveraging AI's power, I specialize in crafting custom prompts and creative
content for AI-driven applications. My services are aimed at businesses, creatives,
and researchers looking to harness AI technology for innovation, efficiency, and
exploring new possibilities.
url: https://promptmaster.nexus
link_text: www.promptmaster.nexus
- icon:
source: https://cloud.veen.world/s/logo_mediator_512x512/download
title: Mediator
text: Specializing in resolving interpersonal and business conflicts with empathy
and neutrality, I facilitate open communication to achieve lasting agreements
and strengthen relationships. My mediation services are designed for individuals,
teams, and organizations to foster a harmonious and productive environment.
url: https://www.mediator.veen.world
link_text: www.mediator.veen.world
- icon:
source: https://cloud.veen.world/s/logo_hypnotherapist_512x512/download
title: Hypnotherapist
text: As a certified Hypnotherapist, I offer tailored sessions to address mental
and emotional challenges through hypnosis. My approach helps unlock the subconscious
to overcome negative beliefs and stress, empowering you to activate self-healing
and embrace positive life changes.
url: https://www.hypno.veen.world
link_text: www.hypno.veen.world
- icon:
source: https://cloud.veen.world/s/logo_skydiver_512x512/download
title: Aerospace Consultant
text: As an Aerospace Consultant with aviation credentials, including a Sport Pilot
License for Parachutes, and a Restricted Radiotelephony and Operator's Certificate
I deliver expert consulting services. Currently training for my Private Pilot
License, I specialize in guiding clients through aviation regulations, safety
standards, and operational efficiency.
url:
link_text: Website under construction
- icon:
source: https://cloud.veen.world/s/logo_hunter_512x512/download
title: Wildlife Expert
text: As a certified hunter and wildlife coach, I offer educational programs, nature
walks, survival trainings, and photo expeditions, merging ecological knowledge
with nature respect. My goal is to foster sustainable conservation and enhance
appreciation for the natural world through responsible practices.
url:
link_text: Website under construction
- icon:
source: https://cloud.veen.world/s/logo_diver_512x512/download
title: Master Diver
text: As a certified master diver with trainings in various specialties, I offer
diving instruction, underwater photography, and guided dive tours. My experience
ensures safe and enriching underwater adventures, highlighting marine conservation
and the wonders of aquatic ecosystems.
url:
link_text: Website under construction
- icon:
source: https://cloud.veen.world/s/logo_massage_therapist_512x512/download
title: Massage Therapist
text: Certified in Tantra Massage, I offer unique full-body rituals to awaken senses
and harmonize body and mind. My sessions, a blend of ancient Tantra and modern
relaxation, focus on energy flow, personal growth, and spiritual awakening.
url:
link_text: Website under construction
company:
titel: Kevin Veen-Birkenbach
subtitel: Consulting and Coaching Solutions
logo:
source: https://cloud.veen.world/s/logo_face_512x512/download
favicon:
source: https://cloud.veen.world/s/veen_world_favicon/download
address:
street: Afrikanische Straße 43
postal_code: DE-13351
city: Berlin
country: Germany
imprint_url: https://s.veen.world/imprint
navigation:
header:
- name: Microblog
description: Read my microblogs
icon:
class: fa-brands fa-mastodon
url: https://microblog.veen.world/@kevinveenbirkenbach
subitems: []
- name: Pictures
description: View my photo gallery
icon:
class: fa-solid fa-camera
url: https://picture.veen.world/kevinveenbirkenbach
subitems: []
- name: Videos
description: Watch my videos
icon:
class: fa-solid fa-video
url: https://video.veen.world/a/kevinveenbirkenbach
subitems: []
- name: Blog
description: Read my blog
icon:
class: fa-solid fa-blog
url: https://blog.veen.world
subitems: []
- name: Code
icon:
class: fa-solid fa-laptop-code
description: Check out my Code
subitems:
- name: Github
description: View my GitHub profile
icon:
class: bi bi-github
url: https://github.com/kevinveenbirkenbach
subitems: []
- name: Gitea
description: Explore my code repositories
icon:
class: fa-solid fa-code
url: https://git.veen.world/kevinveenbirkenbach
subitems: []
- name: Contact
description: Get in touch
icon:
class: fa-solid fa-envelope
subitems:
- name: Email
description: Send me an email
icon:
class: fa-solid fa-envelope
url: mailto:kevin@veen.world
identifier: kevin@veen.world
alternatives:
- link: navigation.header.contact.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
description: Call me
icon:
class: fa-solid fa-phone
url: "tel:+491781798023"
identifier: "+491781798023"
target: _top
- name: Encrypted Email (PGP)
description: Download my PGP key
icon:
class: fa-solid fa-key
url: https://s.veen.world/pgp
identifier: kevin@veen.world
info: |
#### Why Use PGP?
PGP ensures your email content stays private, protecting against surveillance, data breaches, and unauthorized access.
#### Protect Your Privacy
In an age of mass data collection, PGP empowers you to communicate securely and assert control over your information. For insights on protecting your digital rights, visit the [Electronic Frontier Foundation (EFF)](https://www.eff.org/).
#### Build Trust
Encrypting emails demonstrates a commitment to privacy and security, fostering trust in professional and personal communication.
#### 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/).
- name: Signal
description: Message me on Signal
icon:
class: fa-brands fa-signal-messenger
identifier: "+491781798023"
warning: Signal is not hosted by me!
alternatives:
- link: navigation.header.contact.matrix
- name: Telegram
description: Message me on Telegram
icon:
class: fa-brands fa-telegram
target: _blank
url: https://t.me/kevinveenbirkenbach
identifier: kevinveenbirkenbach
warning: Telegram is not hosted by me!
alternatives:
- link: navigation.header.contact.matrix
- name: WhatsApp
description: Chat with me on WhatsApp
icon:
class: fa-brands fa-whatsapp
url: https://wa.me/491781798023
identifier: "+491781798023"
warning: |
⚠️ **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:
- link: navigation.header.contact.matrix
footer:
- name: External Accounts
description: Me on other plattforms
icon:
class: fa-solid fa-external-link-alt
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/
- 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
description: My presence in the Fediverse
icon:
class: fa-solid fa-users
subitems:
- name: Forum
description: Join the discussion
icon:
class: fa-brands fa-discourse
url: https://forum.veen.world/u/kevinveenbirkenbach
subitems: []
- name: Newsletter
description: Subscribe to my newsletter
icon:
class: fa-solid fa-envelope-open-text
url: https://newsletter.veen.world/subscription/form
subitems: []
- name: Work Hub
description: Curated collection of self hosted tools for work, organization, and
learning.
icon:
class: fa-solid fa-toolbox
url:
subitems:
- name: Open Project
description: Explore my projects
icon:
class: fa-solid fa-chart-line
url: https://project.veen.world/
subitems: []
- name: Taiga
description: View my Kanban board
icon:
class: bi bi-clipboard2-check-fill
url: https://kanban.veen.world/
subitems: []
- name: Matomo
description: Analyze with Matomo
icon:
class: fa-solid fa-chart-simple
url: https://matomo.veen.world/
subitems: []
- name: Baserow
description: Organize with Baserow
icon:
class: fa-solid fa-table
url: https://baserow.veen.world/
subitems: []
- name: Elements
description: Chat with me
icon:
class: fa-solid fa-comment
url: https://element.veen.world/
subitems: []
- name: Big Blue Button
description: Join live events
icon:
class: fa-solid fa-video
url: https://meet.veen.world/
subitems: []
- name: Mailu
description: Send me a mail
icon:
class: fa-solid fa-envelope
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
description: Find my curated links
icon:
class: bi bi-link
url: https://s.veen.world/admin/
subitems: []
- name: Nextcloud
description: Access my cloud storage
icon:
class: fa-solid fa-cloud
url: https://cloud.veen.world/
subitems: []
- name: Logbooks
description: My activity logs
icon:
class: fa-solid fa-book
url:
subitems:
- name: Skydiver
description: View my skydiving logs
icon:
class: fa-solid fa-parachute-box
url: https://s.veen.world/skydiverlog
subitems: []
- name: Skipper
description: See my sailing records
icon:
class: fa-solid fa-sailboat
url: https://s.veen.world/meilenbuch
subitems: []
- name: Diver
description: Check my diving logs
icon:
class: fa-solid fa-fish
url: https://s.veen.world/diverlog
subitems: []
- name: Pilot
description: Review my flight logs
icon:
class: fa-solid fa-plane
url: https://s.veen.world/pilotlog
subitems: []
- name: Nature
description: Explore my nature logs
icon:
class: fa-solid fa-tree
url: https://s.veen.world/naturejournal
- name: Vita
description: View my CV and professional background
icon:
class: fa-solid fa-file-lines
url: https://s.veen.world/lebenslauf
subitems: []
- name: Imprint
icon:
class: fa-solid fa-scale-balanced
url: https://s.veen.world/imprint

View File

@ -1,2 +1,3 @@
flask
requests
requests
pyyaml

View File

@ -82,6 +82,15 @@ h3.footer-title{
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;
@ -96,4 +105,35 @@ h3.footer-title{
top: 0;
left: 100%; /* Positioniert das Submenü rechts vom Hauptmenü */
margin-top: -1px;
}
}
.dropdown-menu.collapse {
display: none;
}
.dropdown-menu.collapse.show {
display: block;
}
/* Standardmäßig sind die Submenüs ausgeblendet */
.dropdown-submenu .dropdown-menu {
display: none;
opacity: 0;
transition: opacity 0.3s ease-in-out;
position: absolute;
left: 100%;
top: 0;
}
/* Beim Hover auf das Submenü-Element wird das Menü angezeigt */
.dropdown-submenu:hover > .dropdown-menu {
display: block;
opacity: 1;
z-index: 1050;
}
/* Um sicherzustellen, dass es nicht sofort verschwindet */
.dropdown-submenu:hover > .dropdown-menu:hover {
display: block;
opacity: 1;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,18 +0,0 @@
function openDynamicPopup(subitem) {
// Set modal title and content
document.getElementById('dynamicModalLabel').innerText = subitem.description;
const modalContent = document.getElementById('dynamicModalContent');
modalContent.value = subitem.address;
// Add copy functionality
document.getElementById('dynamicCopyButton').addEventListener('click', function () {
modalContent.select();
navigator.clipboard.writeText(modalContent.value)
.then(() => alert('Content copied to clipboard!'))
.catch(() => alert('Failed to copy content.'));
});
// Show the modal
const modal = new bootstrap.Modal(document.getElementById('dynamicModal'));
modal.show();
}

117
app/static/js/modal.js Normal file
View File

@ -0,0 +1,117 @@
function openDynamicPopup(subitem) {
// Schließe alle offenen Modals
closeAllModals();
// Setze den Titel mit Icon, falls vorhanden
const modalTitle = document.getElementById('dynamicModalLabel');
if (subitem.icon && subitem.icon.class) {
modalTitle.innerHTML = `<i class="${subitem.icon.class}"></i> ${subitem.name}`;
} else {
modalTitle.innerText = subitem.name;
}
// Setze den Identifier, falls vorhanden
const identifierBox = document.getElementById('dynamicIdentifierBox');
const modalContent = document.getElementById('dynamicModalContent');
if (subitem.identifier) {
identifierBox.classList.remove('d-none');
modalContent.value = subitem.identifier;
} else {
identifierBox.classList.add('d-none');
modalContent.value = '';
}
// Konfiguriere die Warnbox mit Markdown
const warningBox = document.getElementById('dynamicModalWarning');
if (subitem.warning) {
warningBox.classList.remove('d-none');
document.getElementById('dynamicModalWarningText').innerHTML = marked.parse(subitem.warning);
} else {
warningBox.classList.add('d-none');
}
// Konfiguriere die Infobox mit Markdown
const infoBox = document.getElementById('dynamicModalInfo');
if (subitem.info) {
infoBox.classList.remove('d-none');
document.getElementById('dynamicModalInfoText').innerHTML = marked.parse(subitem.info);
} else {
infoBox.classList.add('d-none');
}
// Zeige die Beschreibung, falls keine URL vorhanden ist
const descriptionText = document.getElementById('dynamicDescriptionText');
if (!subitem.url && subitem.description) {
descriptionText.classList.remove('d-none');
descriptionText.innerText = subitem.description;
} else {
descriptionText.classList.add('d-none');
descriptionText.innerText = '';
}
// Konfiguriere den Link oder die Beschreibung
const linkBox = document.getElementById('dynamicModalLink');
const linkHref = document.getElementById('dynamicModalLinkHref');
if (subitem.url) {
linkBox.classList.remove('d-none');
linkHref.href = subitem.url;
linkHref.innerText = subitem.description || "Open Link";
} else {
linkBox.classList.add('d-none');
linkHref.href = '#';
}
// Konfiguriere die Alternativen
const alternativesSection = document.getElementById('dynamicAlternativesSection');
const alternativesList = document.getElementById('dynamicAlternativesList');
alternativesList.innerHTML = ''; // Clear existing alternatives
if (subitem.alternatives && subitem.alternatives.length > 0) {
alternativesSection.classList.remove('d-none');
subitem.alternatives.forEach(alt => {
const listItem = document.createElement('li');
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center');
listItem.innerHTML = `
<span>
<i class="${alt.icon.class}"></i> ${alt.name}
</span>
<button class="btn btn-outline-secondary btn-sm">Open</button>
`;
listItem.querySelector('button').addEventListener('click', () => openDynamicPopup(alt));
alternativesList.appendChild(listItem);
});
} else {
alternativesSection.classList.add('d-none');
}
// Kopierfunktion für den Identifier
const copyButton = document.getElementById('dynamicCopyButton');
copyButton.onclick = () => {
modalContent.select();
navigator.clipboard.writeText(modalContent.value).then(() => {
alert('Identifier copied to clipboard!');
});
};
// Modal anzeigen
const modal = new bootstrap.Modal(document.getElementById('dynamicModal'));
modal.show();
}
function closeAllModals() {
const modals = document.querySelectorAll('.modal.show'); // Alle offenen Modals finden
modals.forEach(modal => {
const modalInstance = bootstrap.Modal.getInstance(modal);
if (modalInstance) {
modalInstance.hide(); // Modal ausblenden
}
});
// Entferne die "modal-backdrop"-Elemente
const backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(backdrop => backdrop.remove());
// Entferne die Klasse, die den Hintergrund ausgraut
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
}

28
app/static/js/submenus.js Normal file
View File

@ -0,0 +1,28 @@
document.addEventListener('DOMContentLoaded', () => {
const dropdownSubmenus = document.querySelectorAll('.dropdown-submenu');
dropdownSubmenus.forEach(submenu => {
let timeout;
// Zeige das Submenü beim Hover
submenu.addEventListener('mouseenter', () => {
clearTimeout(timeout);
const menu = submenu.querySelector('.dropdown-menu');
if (menu) {
menu.style.display = 'block';
menu.style.opacity = '1';
}
});
// Verstecke das Submenü nach 0.5 Sekunden
submenu.addEventListener('mouseleave', () => {
const menu = submenu.querySelector('.dropdown-menu');
if (menu) {
timeout = setTimeout(() => {
menu.style.display = 'none';
menu.style.opacity = '0';
}, 500); // 0.5 Sekunden Verzögerung
}
});
});
});

View File

@ -3,7 +3,7 @@
<head>
<title>{{company.titel}}</title>
<meta charset="utf-8" >
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="icon" type="image/x-icon" href="{{company.favicon.cache}}">
<!-- Bootstrap CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<!-- Bootstrap JavaScript Bundle with Popper -->
@ -12,12 +12,14 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
<!-- Fontawesome -->
<script src="https://kit.fontawesome.com/56f96da298.js" crossorigin="anonymous"></script>
<!-- Markdown -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="static/css/default.css">
</head>
<body>
<div class="container">
<header class="header">
<img src="{{company.logo}}" alt="logo"/>
<img src="{{company.logo.cache}}" alt="logo"/>
<h1>{{company.titel}}</h1>
<h2>{{company.subtitel}}</h2>
<br />
@ -38,7 +40,8 @@
</div>
<!-- Include modal -->
{% include "moduls/modal.html.j2" %}
<script src="{{ url_for('static', filename='js/dynamic-modal.js') }}"></script>
<script src="{{ url_for('static', filename='js/modal.js') }}"></script>
<script src="{{ url_for('static', filename='js/submenus.js') }}"></script>
<script src="{{ url_for('static', filename='js/tooltip.js') }}"></script>
</body>
</html>

View File

@ -2,7 +2,7 @@
<div class="card h-100 d-flex flex-column">
<div class="card-body d-flex flex-column">
<div class="card-img-top">
<img src="{{ card.icon }}" alt="{{ card.title }}" style="width: 100px; height: auto;">
<img src="{{ card.icon.cache }}" alt="{{ card.title }}" style="width: 100px; height: auto;">
</div>
<hr />
<h3 class="card-title">{{ card.title }}</h3>

View File

@ -1,4 +1,3 @@
<!-- Universal Modal Structure -->
<div class="modal fade" id="dynamicModal" tabindex="-1" aria-labelledby="dynamicModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
@ -7,14 +6,34 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group">
<!-- Warnbox mit Markdown -->
<div id="dynamicModalWarning" class="alert alert-warning d-none" role="alert">
<h5><i class="fa-solid fa-triangle-exclamation"></i> Warning </h5><span id="dynamicModalWarningText"></span>
</div>
<!-- Infobox mit Markdown -->
<div id="dynamicModalInfo" class="alert alert-info d-none" role="alert">
<h5><i class="fa-solid fa-circle-info"></i> Information</h5><span id="dynamicModalInfoText"></span>
</div>
<!-- Description text -->
<div id="dynamicDescriptionText" class="mt-2 d-none"></div>
<!-- Eingabebox für Identifier -->
<div id="dynamicIdentifierBox" class="input-group mt-2 d-none">
<input type="text" id="dynamicModalContent" class="form-control" readonly>
<button class="btn btn-outline-secondary" type="button" id="dynamicCopyButton">Copy</button>
</div>
<!-- Link -->
<div id="dynamicModalLink" class="mt-3 d-none">
<a href="#" target="_blank" class="btn btn-primary w-100" id="dynamicModalLinkHref"></a>
</div>
<!-- Alternativen -->
<div id="dynamicAlternativesSection" class="mt-4 d-none">
<h6>Alternatives:</h6>
<ul class="list-group" id="dynamicAlternativesList"></ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="closeAllModals()">Close</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -2,24 +2,28 @@
{% macro render_subitems(subitems) %}
{% for subitem in subitems %}
{% if subitem.subitems %}
<li class="dropdown-submenu">
<a class="dropdown-item" title="{{ subitem.description }}">
<i class="{{ subitem.icon_class }}"></i> {{ subitem.name }}
<li class="dropdown-submenu position-relative">
<a class="dropdown-item dropdown-toggle" href="#" title="{{ subitem.description }}">
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
</a>
<ul class="dropdown-submenu">
<ul class="dropdown-menu">
{{ render_subitems(subitem.subitems) }}
</ul>
</li>
{% elif subitem.popup %}
{% elif subitem.identifier %}
<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({{ 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="{{ subitem.href }}" target="{{ subitem.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ subitem.description }}">
<i class="{{ subitem.icon_class }}"></i> {{ subitem.name }}
<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>Fehlendes Icon im Subitem: {{ subitem }}</p>
{% endif %}
</a>
</li>
{% endif %}
@ -32,24 +36,24 @@
<!--
<a class="navbar-brand" href="#">Navbar</a>
-->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" 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>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<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] %}
{% if item.href %}
{% if item.url %}
<!-- Single Item -->
<li class="nav-item">
<a class="nav-link" href="{{ item.href }}" target="{{ item.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ item.description }}">
<i class="{{ item.icon_class }}"></i> {{ item.name }}
<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 }}
</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" data-popper-placement="top" title="{{ item.description }}" aria-expanded="false">
<i class="{{ item.icon_class }}" data-bs-toggle="tooltip"></i> {{ item.name }}
<i class="{{ item.icon.class }}" data-bs-toggle="tooltip"></i> {{ item.name }}
</a>
<ul class="dropdown-menu dropdown-menu-{{menu_type}}" aria-labelledby="navbarDropdown{{ loop.index }}">
{{ render_subitems(item.subitems) }}

View File

@ -0,0 +1,68 @@
import os
import hashlib
import requests
class CacheManager:
"""
A class to manage caching of files, including creating temporary directories
and caching files locally with hashed filenames.
"""
def __init__(self, cache_dir="static/cache"):
"""
Initialize the CacheManager with a cache directory.
:param cache_dir: The directory where cached files will be stored.
"""
self.cache_dir = cache_dir
self._ensure_cache_dir_exists()
def _ensure_cache_dir_exists(self):
"""
Ensure the cache directory exists. If it doesn't, create it.
"""
if not os.path.exists(self.cache_dir):
os.makedirs(self.cache_dir)
print(f"Created cache directory: {self.cache_dir}")
def clear_cache(self):
"""
Clear all files in the cache directory.
"""
if os.path.exists(self.cache_dir):
for filename in os.listdir(self.cache_dir):
file_path = os.path.join(self.cache_dir, filename)
if os.path.isfile(file_path):
os.remove(file_path)
print(f"Deleted: {file_path}")
def cache_file(self, file_url):
"""
Download a file and store it locally in the cache directory with a hashed filename.
:param file_url: The URL of the file to cache.
:return: The local path of the cached file.
"""
# Generate a hashed filename based on the URL
hash_object = hashlib.blake2s(file_url.encode('utf-8'), digest_size=8)
hash_suffix = hash_object.hexdigest()
# Determine the base name for the file
splitted_file_url = file_url.split("/")
base_name = splitted_file_url[-2] if splitted_file_url[-1] == "download" else splitted_file_url[-1]
# Construct the full path for the cached file
filename = f"{base_name}_{hash_suffix}.png"
full_path = os.path.join(self.cache_dir, filename)
# If the file already exists, return the cached path
if os.path.exists(full_path):
return full_path
# Download the file and save it locally
response = requests.get(file_url, stream=True)
if response.status_code == 200:
with open(full_path, "wb") as file:
for chunk in response.iter_content(1024):
file.write(chunk)
return full_path

View File

@ -0,0 +1,88 @@
class ConfigurationResolver:
"""
A class to resolve `link` entries in a nested configuration structure.
Supports navigation through dictionaries, lists, and `subitems`.
"""
def __init__(self, config):
self.config = config
def resolve_links(self):
"""
Resolves all `link` entries in the configuration.
"""
self._recursive_resolve(self.config, self.config)
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":
try:
target = self._find_entry(root_config, value.lower())
current_config.clear()
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}"
)
else:
self._recursive_resolve(value, root_config)
elif isinstance(current_config, list):
for item in current_config:
self._recursive_resolve(item, root_config)
def _find_entry(self, config, path):
"""
Finds an entry in the configuration by a dot-separated path.
Supports both dictionaries and lists with `subitems` navigation.
"""
parts = path.split('.')
current = config
for part in parts:
if isinstance(current, list):
# Look for a matching name in the list
found = next(
(item for item in current if isinstance(item, dict) and item.get("name", "").lower() == part),
None
)
if found:
print(
f"Matching entry for '{part}' in list. Path so far: {' > '.join(parts[:parts.index(part)+1])}. "
f"Current list: {current}"
)
else:
raise ValueError(
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:
raise KeyError(
f"Key '{part}' not found in dictionary. Path so far: {' > '.join(parts[:parts.index(part)+1])}. "
f"Current dictionary: {current}"
)
current = current[key]
else:
raise ValueError(
f"Invalid path segment '{part}'. Current type: {type(current)}. "
f"Path so far: {' > '.join(parts[:parts.index(part)+1])}"
)
# Navigate into `subitems` if present
if isinstance(current, dict) and ("subitems" in current and current["subitems"]):
current = current["subitems"]
return current
def get_config(self):
"""
Returns the resolved configuration.
"""
return self.config