mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2025-06-28 23:55:31 +02:00
Compare commits
20 Commits
00e8047fb7
...
3c240fc16b
Author | SHA1 | Date | |
---|---|---|---|
3c240fc16b | |||
2a3491b98b | |||
378ee4632f | |||
19f99ff9d3 | |||
a9fcd4b6de | |||
e303968ca5 | |||
f85dc5bb18 | |||
562f5989e1 | |||
9455f40079 | |||
d59cc73470 | |||
7a66184a46 | |||
d8ec067675 | |||
c87c1df10a | |||
8fb0cecfbe | |||
61af45e837 | |||
4ee5340dd3 | |||
14ccedf1c1 | |||
8959f4405b | |||
e45bd16631 | |||
9b763cd34b |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
||||
app/static/logos/*
|
||||
app/static/cache/*
|
||||
*__pycache__*
|
36
README.md
36
README.md
@ -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
|
||||
```
|
76
app/app.py
76
app/app.py
@ -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)
|
||||
|
459
app/config.json
459
app/config.json
@ -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
460
app/config.yaml
Normal 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
|
@ -1,2 +1,3 @@
|
||||
flask
|
||||
requests
|
||||
requests
|
||||
pyyaml
|
@ -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 |
@ -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
117
app/static/js/modal.js
Normal 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
28
app/static/js/submenus.js
Normal 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
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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) }}
|
||||
|
68
app/utils/cache_manager.py
Normal file
68
app/utils/cache_manager.py
Normal 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
|
88
app/utils/configuration_resolver.py
Normal file
88
app/utils/configuration_resolver.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user