Compare commits

...

10 Commits

6 changed files with 237 additions and 86 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
app/static/cache/*
app/config.yaml
*__pycache__*
app/static/cache/*

169
README.md
View File

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

View File

@ -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);
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 {
warningBox.classList.add('d-none');
box.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');
}
toggleBox('dynamicModalWarning', 'dynamicModalWarningText', subitem.warning);
toggleBox('dynamicModalInfo', 'dynamicModalInfoText', subitem.info);
// 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 = '#';
}
function populateSection(sectionId, listId, items, onClickHandler) {
const section = document.getElementById(sectionId);
const list = document.getElementById(listId);
list.innerHTML = '';
// 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 => {
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="${alt.icon.class}"></i> ${alt.name}
<i class="${item.icon.class}"></i> ${item.name}
</span>
<button class="btn btn-outline-secondary btn-sm">Open</button>
`;
listItem.querySelector('button').addEventListener('click', () => openDynamicPopup(alt));
alternativesList.appendChild(listItem);
listItem.querySelector('button').addEventListener('click', () => onClickHandler(item));
list.appendChild(listItem);
});
} else {
alternativesSection.classList.add('d-none');
section.classList.add('d-none');
}
}
// Kopierfunktion für den Identifier
populateSection('dynamicAlternativesSection', 'dynamicAlternativesList', subitem.alternatives, openDynamicPopup);
populateSection('dynamicChildrenSection', 'dynamicChildrenList', subitem.children, openDynamicPopup);
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 = '';

View File

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

View File

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