mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2025-04-28 23:41:56 +02:00
Compare commits
10 Commits
11eccf2eca
...
dc11dc799b
Author | SHA1 | Date | |
---|---|---|---|
dc11dc799b | |||
8c7dc02bd5 | |||
9741da0495 | |||
0f8113974f | |||
a0664691e6 | |||
0360c443b7 | |||
954cff051a | |||
7f78e77a10 | |||
1c6b70d640 | |||
f664270b5d |
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
app/static/cache/*
|
||||
*__pycache__*
|
||||
app/config.yaml
|
||||
*__pycache__*
|
||||
app/static/cache/*
|
171
README.md
171
README.md
@ -1,37 +1,182 @@
|
||||
# Landingpage
|
||||
# Portfolio: Flask-based Portfolio 🚀
|
||||
|
||||
This software allows individuals and institutions to set up an easy portfolio/landingpage/homepage to showcase their projects and online presence. It is highly customizable via a YAML configuration file.
|
||||
|
||||
## Features ✨
|
||||
|
||||
- **Dynamic Navigation**: Easily create dropdown menus and nested links.
|
||||
- **Customizable Cards**: Showcase your skills, projects, or services.
|
||||
- **Cache Management**: Optimize your assets with automatic caching.
|
||||
- **Responsive Design**: Beautiful on any device with Bootstrap.
|
||||
- **Easy Configuration**: Update content using a YAML file.
|
||||
|
||||
## Access 🌐
|
||||
|
||||
## Access
|
||||
### Locale
|
||||
[http://127.0.0.1:5000](http://127.0.0.1:5000)
|
||||
Access the application locally at [http://127.0.0.1:5000](http://127.0.0.1:5000).
|
||||
|
||||
## Getting Started 🏁
|
||||
|
||||
### Prerequisites 📋
|
||||
|
||||
- Docker and Docker Compose installed on your system.
|
||||
- Basic knowledge of Python and YAML for configuration.
|
||||
|
||||
### Installation 🛠️
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone <repository_url>
|
||||
cd <repository_directory>
|
||||
```
|
||||
|
||||
2. **Update the configuration:**
|
||||
Create a `config.yaml` file. You can use `config.sample.yaml` as an example (see below for details on the configuration).
|
||||
|
||||
3. **Build and run the Docker container:**
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
4. **Access your portfolio:** Open your browser and navigate to `http://localhost:5000`.
|
||||
|
||||
## Configuration Guide 🔧
|
||||
|
||||
The portfolio is powered by a YAML configuration file (`config.yaml`). This file allows you to define the structure and content of your site, including cards, navigation, and company details.
|
||||
|
||||
### YAML Configuration Example 📄
|
||||
|
||||
```yaml
|
||||
accounts:
|
||||
name: Online Accounts
|
||||
description: Discover my online presence.
|
||||
icon:
|
||||
class: fa-solid fa-users
|
||||
children:
|
||||
- name: Channels
|
||||
description: Platforms where I share content.
|
||||
icon:
|
||||
class: fas fa-newspaper
|
||||
children:
|
||||
- name: Microblogs
|
||||
description: Stay updated with my microblog posts.
|
||||
icon:
|
||||
class: fa-solid fa-pen-nib
|
||||
children:
|
||||
- name: Mastodon
|
||||
description: Follow my updates on Mastodon.
|
||||
icon:
|
||||
class: fa-brands fa-mastodon
|
||||
url: https://microblog.veen.world/@kevinveenbirkenbach
|
||||
identifier: "@kevinveenbirkenbach@microblog.veen.world"
|
||||
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 and Agile Coaching.
|
||||
url: https://www.agile-coach.world
|
||||
link_text: www.agile-coach.world
|
||||
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
|
||||
```
|
||||
|
||||
### Understanding the `children` Key 🔍
|
||||
|
||||
The `children` key allows hierarchical nesting of elements. Each child can itself have children, enabling the creation of multi-level navigation menus or grouped content. Example:
|
||||
|
||||
```yaml
|
||||
children:
|
||||
- name: Parent Item
|
||||
description: Parent description.
|
||||
icon:
|
||||
class: fa-solid fa-folder
|
||||
children:
|
||||
- name: Child Item
|
||||
description: Child description.
|
||||
icon:
|
||||
class: fa-solid fa-file
|
||||
url: https://example.com
|
||||
```
|
||||
|
||||
This structure will render a parent menu or section containing nested child elements. Each child can be further customized with icons, descriptions, and links.
|
||||
|
||||
### Understanding the `link` Key 🔗
|
||||
|
||||
The `link` key allows you to reference another part of the YAML configuration by its path. This is useful for avoiding duplication and maintaining consistency. Example:
|
||||
|
||||
```yaml
|
||||
children:
|
||||
- name: Blog
|
||||
description: My blog posts.
|
||||
icon:
|
||||
class: fa-solid fa-blog
|
||||
url: https://example.com/blog
|
||||
- name: Featured Blog
|
||||
link: accounts.children[0].children[0] # References the "Blog" item above
|
||||
```
|
||||
|
||||
In this example, `Featured Blog` will inherit all properties from the `Blog` item, including its name, description, and URL. This feature ensures that any updates to the `Blog` item are automatically reflected in all linked entries.
|
||||
|
||||
## Administrate Docker 🐳
|
||||
|
||||
## Administrate Docker
|
||||
### Stop and Destroy
|
||||
```bash
|
||||
docker stop landingpage
|
||||
docker rm landingpage
|
||||
```bash
|
||||
docker stop portfolio; docker rm portfolio
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
docker build -t application-landingpage .
|
||||
docker build -t application-portfolio .
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
#### Run Development Environment
|
||||
```bash
|
||||
docker run -d -p 5000:5000 --name landingpage -v $(pwd)/app/:/app -e FLASK_APP=app.py -e FLASK_ENV=development application-landingpage
|
||||
docker run -d -p 5000:5000 --name portfolio -v $(pwd)/app/:/app -e FLASK_APP=app.py -e FLASK_ENV=development application-portfolio
|
||||
```
|
||||
|
||||
#### Run Production Environment
|
||||
```bash
|
||||
docker run -d -p 5000:5000 --name landingpage application-landingpage
|
||||
docker run -d -p 5000:5000 --name portfolio application-portfolio
|
||||
```
|
||||
|
||||
### Debug
|
||||
```bash
|
||||
docker logs -f landingpage
|
||||
docker logs -f portfolio
|
||||
```
|
||||
## Author
|
||||
This software was created from [Kevin Veen-Birkenbach](https://www.veen.world/) with the help of [ChatGPT]()
|
||||
|
||||
## Development Mode 🧑💻
|
||||
|
||||
To run the app in development mode with hot-reloading:
|
||||
|
||||
```bash
|
||||
FLASK_ENV=development docker-compose up
|
||||
```
|
||||
|
||||
## Deployment 🚢
|
||||
|
||||
For production deployment, ensure to:
|
||||
|
||||
- Use a reverse proxy like NGINX or Apache.
|
||||
- Secure your site with SSL/TLS.
|
||||
- Use a production-ready database if required.
|
||||
|
||||
## Author ✍️
|
||||
|
||||
This software was created by [Kevin Veen-Birkenbach](https://www.veen.world/).
|
||||
|
||||
## License 📜
|
||||
|
||||
This project is licensed under the GNU Affero General Public License Version 3. See the [LICENSE](./LICENSE) file for details.
|
@ -1,8 +1,5 @@
|
||||
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}`;
|
||||
@ -10,7 +7,6 @@ function openDynamicPopup(subitem) {
|
||||
modalTitle.innerText = subitem.name;
|
||||
}
|
||||
|
||||
// Setze den Identifier, falls vorhanden
|
||||
const identifierBox = document.getElementById('dynamicIdentifierBox');
|
||||
const modalContent = document.getElementById('dynamicModalContent');
|
||||
if (subitem.identifier) {
|
||||
@ -21,25 +17,19 @@ function openDynamicPopup(subitem) {
|
||||
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');
|
||||
function toggleBox(boxId, textId, content) {
|
||||
const box = document.getElementById(boxId);
|
||||
if (content) {
|
||||
box.classList.remove('d-none');
|
||||
document.getElementById(textId).innerHTML = marked.parse(content);
|
||||
} else {
|
||||
box.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
toggleBox('dynamicModalWarning', 'dynamicModalWarningText', subitem.warning);
|
||||
toggleBox('dynamicModalInfo', 'dynamicModalInfoText', subitem.info);
|
||||
|
||||
// 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');
|
||||
@ -49,7 +39,6 @@ function openDynamicPopup(subitem) {
|
||||
descriptionText.innerText = '';
|
||||
}
|
||||
|
||||
// Konfiguriere den Link oder die Beschreibung
|
||||
const linkBox = document.getElementById('dynamicModalLink');
|
||||
const linkHref = document.getElementById('dynamicModalLinkHref');
|
||||
if (subitem.url) {
|
||||
@ -60,30 +49,33 @@ function openDynamicPopup(subitem) {
|
||||
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');
|
||||
function populateSection(sectionId, listId, items, onClickHandler) {
|
||||
const section = document.getElementById(sectionId);
|
||||
const list = document.getElementById(listId);
|
||||
list.innerHTML = '';
|
||||
|
||||
if (items && items.length > 0) {
|
||||
section.classList.remove('d-none');
|
||||
items.forEach(item => {
|
||||
const listItem = document.createElement('li');
|
||||
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center');
|
||||
listItem.innerHTML = `
|
||||
<span>
|
||||
<i class="${item.icon.class}"></i> ${item.name}
|
||||
</span>
|
||||
<button class="btn btn-outline-secondary btn-sm">Open</button>
|
||||
`;
|
||||
listItem.querySelector('button').addEventListener('click', () => onClickHandler(item));
|
||||
list.appendChild(listItem);
|
||||
});
|
||||
} else {
|
||||
section.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
populateSection('dynamicAlternativesSection', 'dynamicAlternativesList', subitem.alternatives, openDynamicPopup);
|
||||
populateSection('dynamicChildrenSection', 'dynamicChildrenList', subitem.children, openDynamicPopup);
|
||||
|
||||
// Kopierfunktion für den Identifier
|
||||
const copyButton = document.getElementById('dynamicCopyButton');
|
||||
copyButton.onclick = () => {
|
||||
modalContent.select();
|
||||
@ -92,25 +84,20 @@ function openDynamicPopup(subitem) {
|
||||
});
|
||||
};
|
||||
|
||||
// Modal anzeigen
|
||||
const modal = new bootstrap.Modal(document.getElementById('dynamicModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
const modals = document.querySelectorAll('.modal.show'); // Alle offenen Modals finden
|
||||
const modals = document.querySelectorAll('.modal.show');
|
||||
modals.forEach(modal => {
|
||||
const modalInstance = bootstrap.Modal.getInstance(modal);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide(); // Modal ausblenden
|
||||
modalInstance.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 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 = '';
|
||||
|
@ -1,3 +1,16 @@
|
||||
{% macro alert_box(id, alert_class, icon_class, title, text_id) %}
|
||||
<div id="{{ id }}" class="alert {{ alert_class }} d-none" role="alert">
|
||||
<h5><i class="{{ icon_class }}"></i> {{ title }} </h5><span id="{{ text_id }}"></span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro list_section(id, title, list_id) %}
|
||||
<div id="{{ id }}" class="mt-4 d-none">
|
||||
<h6>{{ title }}:</h6>
|
||||
<ul class="list-group" id="{{ list_id }}"></ul>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="modal fade" id="dynamicModal" tabindex="-1" aria-labelledby="dynamicModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
@ -6,30 +19,31 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 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>
|
||||
<!-- Warning box with Markdown -->
|
||||
{{ alert_box('dynamicModalWarning', 'alert-warning', 'fa-solid fa-triangle-exclamation', 'Warning', 'dynamicModalWarningText') }}
|
||||
|
||||
<!-- Info box with Markdown -->
|
||||
{{ alert_box('dynamicModalInfo', 'alert-info', 'fa-solid fa-circle-info', 'Information', 'dynamicModalInfoText') }}
|
||||
|
||||
<!-- Description text -->
|
||||
<div id="dynamicDescriptionText" class="mt-2 d-none"></div>
|
||||
<!-- Eingabebox für Identifier -->
|
||||
|
||||
<!-- Input box for 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>
|
||||
|
||||
<!-- Options -->
|
||||
{{ list_section('dynamicChildrenSection', 'Options', 'dynamicChildrenList') }}
|
||||
|
||||
<!-- Alternatives -->
|
||||
{{ list_section('dynamicAlternativesSection', 'Alternatives', 'dynamicAlternativesList') }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="closeAllModals()">Close</button>
|
||||
|
@ -46,7 +46,7 @@ class ConfigurationResolver:
|
||||
raise ValueError(f"Expected 'children' to be a list, but got {type(value).__name__} instead.")
|
||||
for item in value:
|
||||
if "link" in item:
|
||||
loaded_link = self._find_entry(root_config, item['link'].lower(), False)
|
||||
loaded_link = self._find_entry(root_config, self._mapped_key(item['link']), False)
|
||||
if isinstance(loaded_link, list):
|
||||
self._replace_in_list_by_list(value,item,loaded_link)
|
||||
else:
|
||||
@ -55,15 +55,15 @@ class ConfigurationResolver:
|
||||
self._recursive_resolve(value, root_config)
|
||||
elif key == "link":
|
||||
try:
|
||||
loaded = self._find_entry(root_config, value.lower(), True)
|
||||
loaded = self._find_entry(root_config, self._mapped_key(value), True)
|
||||
if isinstance(loaded, list) and len(loaded) > 2:
|
||||
loaded = self._find_entry(root_config, value.lower(), False)
|
||||
loaded = self._find_entry(root_config, self._mapped_key(value), False)
|
||||
current_config.clear()
|
||||
current_config.update(loaded)
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Error resolving link '{value}': {str(e)}. "
|
||||
f"Current path: {key}, Current config: {current_config}" + (f", Loaded: {loaded}" if 'loaded' in locals() or 'loaded' in globals() else "")
|
||||
f"Current part: {key}, Current config: {current_config}" + (f", Loaded: {loaded}" if 'loaded' in locals() or 'loaded' in globals() else "")
|
||||
)
|
||||
else:
|
||||
self._recursive_resolve(value, root_config)
|
||||
@ -76,9 +76,12 @@ class ConfigurationResolver:
|
||||
current = current["children"]
|
||||
return current
|
||||
|
||||
def _mapped_key(self,name):
|
||||
return name.replace(" ", "").lower()
|
||||
|
||||
def _find_by_name(self,current, part):
|
||||
return next(
|
||||
(item for item in current if isinstance(item, dict) and item.get("name", "").lower() == part),
|
||||
(item for item in current if isinstance(item, dict) and self._mapped_key(item.get("name", "")) == part),
|
||||
None
|
||||
)
|
||||
|
||||
@ -108,7 +111,8 @@ class ConfigurationResolver:
|
||||
)
|
||||
elif isinstance(current, dict):
|
||||
# Case-insensitive dictionary lookup
|
||||
key = next((k for k in current if k.lower() == part), None)
|
||||
key = next((k for k in current if self._mapped_key(k) == part), None)
|
||||
# If no fitting key was found search in the children
|
||||
if key is None:
|
||||
current = self._find_by_name(current["children"],part)
|
||||
if not current:
|
||||
|
Loading…
x
Reference in New Issue
Block a user