mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2025-09-09 19:27:11 +02:00
Compare commits
11 Commits
35bfeeb51e
...
20b6c731b8
Author | SHA1 | Date | |
---|---|---|---|
20b6c731b8 | |||
2f63009c31 | |||
f0d4206731 | |||
b8aad8b695 | |||
697696347f | |||
d6389157ec | |||
25dbc3f331 | |||
bb8799eb8a | |||
86fd72b623 | |||
9c24a8658f | |||
5fc19f6ccb |
@@ -11,11 +11,4 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
# Copy application code
|
# Copy application code
|
||||||
COPY app/ .
|
COPY app/ .
|
||||||
|
|
||||||
# Set default port environment variable
|
CMD ["python", "app.py"]
|
||||||
ENV PORT=5000
|
|
||||||
|
|
||||||
# Expose port (optional for documentation)
|
|
||||||
EXPOSE ${PORT}
|
|
||||||
|
|
||||||
# Start command using shell to allow env substitution
|
|
||||||
CMD ["sh", "-c", "exec python app.py --port=${PORT}"]
|
|
||||||
|
12
app/app.py
12
app/app.py
@@ -4,6 +4,12 @@ import yaml
|
|||||||
from utils.configuration_resolver import ConfigurationResolver
|
from utils.configuration_resolver import ConfigurationResolver
|
||||||
from utils.cache_manager import CacheManager
|
from utils.cache_manager import CacheManager
|
||||||
from utils.compute_card_classes import compute_card_classes
|
from utils.compute_card_classes import compute_card_classes
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
FLASK_ENV = os.getenv("FLASK_ENV", "production")
|
||||||
|
FLASK_PORT = int(os.getenv("PORT", 5000))
|
||||||
|
print(f"🔧 Starting app on port {FLASK_PORT}, FLASK_ENV={FLASK_ENV}")
|
||||||
|
|
||||||
|
|
||||||
# Initialize the CacheManager
|
# Initialize the CacheManager
|
||||||
cache_manager = CacheManager()
|
cache_manager = CacheManager()
|
||||||
@@ -31,9 +37,6 @@ def cache_icons_and_logos(app):
|
|||||||
app.config["platform"]["favicon"]["cache"] = cache_manager.cache_file(app.config["platform"]["favicon"]["source"])
|
app.config["platform"]["favicon"]["cache"] = cache_manager.cache_file(app.config["platform"]["favicon"]["source"])
|
||||||
app.config["platform"]["logo"]["cache"] = cache_manager.cache_file(app.config["platform"]["logo"]["source"])
|
app.config["platform"]["logo"]["cache"] = cache_manager.cache_file(app.config["platform"]["logo"]["source"])
|
||||||
|
|
||||||
# Get the environment variable FLASK_ENV or set a default value
|
|
||||||
FLASK_ENV = os.getenv("FLASK_ENV", "production")
|
|
||||||
|
|
||||||
# Initialize Flask app
|
# Initialize Flask app
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@@ -64,5 +67,4 @@ def index():
|
|||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
port = int(os.getenv("PORT", 5000))
|
app.run(debug=(FLASK_ENV == "development"), host="0.0.0.0", port=FLASK_PORT)
|
||||||
app.run(debug=(FLASK_ENV == "development"), host="0.0.0.0", port=port)
|
|
||||||
|
@@ -647,4 +647,36 @@ navigation:
|
|||||||
class: fa-solid fa-scale-balanced
|
class: fa-solid fa-scale-balanced
|
||||||
url: https://s.veen.world/imprint
|
url: https://s.veen.world/imprint
|
||||||
iframe: true
|
iframe: true
|
||||||
|
- name: Settings
|
||||||
|
description: Application settings
|
||||||
|
icon:
|
||||||
|
class: fa-solid fa-cog
|
||||||
|
children:
|
||||||
|
- name: Toggle Fullscreen
|
||||||
|
description: Enter or exit fullscreen mode
|
||||||
|
icon:
|
||||||
|
class: fa-solid fa-expand-arrows-alt
|
||||||
|
onclick: "toggleFullscreen()"
|
||||||
|
- name: Toggle Full Width
|
||||||
|
description: Switch between normal and full-width layout
|
||||||
|
icon:
|
||||||
|
class: fa-solid fa-arrows-left-right
|
||||||
|
onclick: "setFullWidth(!initFullWidthFromUrl())"
|
||||||
|
- name: Open in new tab
|
||||||
|
description: Open the currently embedded iframe URL in a fresh browser tab
|
||||||
|
icon:
|
||||||
|
class: fa-solid fa-up-right-from-square
|
||||||
|
onclick: openIframeInNewTab()
|
||||||
|
- name: Print
|
||||||
|
description: Print the current view
|
||||||
|
icon:
|
||||||
|
class: fa-solid fa-print
|
||||||
|
onclick: window.print()
|
||||||
|
- name: Zoom +
|
||||||
|
icon:
|
||||||
|
class: fa-solid fa-search-plus
|
||||||
|
onclick: zoomPage(1.1)
|
||||||
|
- name: Zoom –
|
||||||
|
icon:
|
||||||
|
class: fa-solid fa-search-minus
|
||||||
|
onclick: zoomPage(0.9)
|
||||||
|
86
app/static/js/fullscreen.js
Normal file
86
app/static/js/fullscreen.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Add or remove the `fullscreen=1` URL parameter.
|
||||||
|
* @param {boolean} enabled
|
||||||
|
*/
|
||||||
|
function updateUrlFullscreen(enabled) {
|
||||||
|
var url = new URL(window.location);
|
||||||
|
if (enabled) url.searchParams.set('fullscreen', '1');
|
||||||
|
else url.searchParams.delete('fullscreen');
|
||||||
|
window.history.replaceState({}, '', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enter fullscreen: hide header/footer, enable full width, recalc scroll,
|
||||||
|
* set both URL params, update button.
|
||||||
|
*/
|
||||||
|
function enterFullscreen() {
|
||||||
|
document.querySelectorAll('header, footer')
|
||||||
|
.forEach(function(el){ el.style.display = 'none'; });
|
||||||
|
setFullWidth(true);
|
||||||
|
updateUrlFullscreen(true);
|
||||||
|
|
||||||
|
if (typeof adjustScrollContainerHeight === 'function') adjustScrollContainerHeight();
|
||||||
|
if (typeof updateCustomScrollbar === 'function') updateCustomScrollbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exit fullscreen: show header/footer, disable full width, recalc scroll,
|
||||||
|
* clear both URL params, update button.
|
||||||
|
*/
|
||||||
|
function exitFullscreen() {
|
||||||
|
document.querySelectorAll('header, footer')
|
||||||
|
.forEach(function(el){ el.style.display = ''; });
|
||||||
|
setFullWidth(false);
|
||||||
|
updateUrlFullscreen(false);
|
||||||
|
|
||||||
|
if (typeof adjustScrollContainerHeight === 'function') adjustScrollContainerHeight();
|
||||||
|
if (typeof updateCustomScrollbar === 'function') updateCustomScrollbar();
|
||||||
|
if (typeof syncIframeHeight === 'function') syncIframeHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle between enter and exit fullscreen.
|
||||||
|
*/
|
||||||
|
function toggleFullscreen() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const isFull = params.get('fullscreen') === '1';
|
||||||
|
|
||||||
|
if (isFull) exitFullscreen();
|
||||||
|
else enterFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read `fullscreen` flag from URL on load.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function initFullscreenFromUrl() {
|
||||||
|
return new URLSearchParams(window.location.search).get('fullscreen') === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
// On page load: apply fullwidth & fullscreen flags
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// first fullwidth
|
||||||
|
var wasFullWidth = initFullWidthFromUrl();
|
||||||
|
setFullWidth(wasFullWidth);
|
||||||
|
|
||||||
|
// now fullscreen
|
||||||
|
if (initFullscreenFromUrl()) {
|
||||||
|
enterFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mirror native F11/fullscreen API events
|
||||||
|
document.addEventListener('fullscreenchange', function() {
|
||||||
|
if (document.fullscreenElement) enterFullscreen();
|
||||||
|
else exitFullscreen();
|
||||||
|
});
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
var isUiFs = Math.abs(window.innerHeight - screen.height) < 2;
|
||||||
|
if (isUiFs) enterFullscreen();
|
||||||
|
else exitFullscreen();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose globally
|
||||||
|
window.fullscreen = enterFullscreen;
|
||||||
|
window.exitFullscreen = exitFullscreen;
|
||||||
|
window.toggleFullscreen = toggleFullscreen;
|
42
app/static/js/fullwidth.js
Normal file
42
app/static/js/fullwidth.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Toggles the .container class between .container and .container-fluid.
|
||||||
|
* @param {boolean} enabled – true = full width, false = normal.
|
||||||
|
*/
|
||||||
|
function setFullWidth(enabled) {
|
||||||
|
var el = document.querySelector('.container, .container-fluid');
|
||||||
|
if (!el) return;
|
||||||
|
console.log(el)
|
||||||
|
if (enabled) {
|
||||||
|
el.classList.replace('container', 'container-fluid');
|
||||||
|
updateUrlFullWidth(true)
|
||||||
|
} else {
|
||||||
|
el.classList.replace('container-fluid', 'container');
|
||||||
|
updateUrlFullWidth(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the URL parameter `fullwidth` and applies full width if it's set.
|
||||||
|
* @returns {boolean} – current full‐width state
|
||||||
|
*/
|
||||||
|
function initFullWidthFromUrl() {
|
||||||
|
var isFull = new URLSearchParams(window.location.search).get('fullwidth') === '1';
|
||||||
|
setFullWidth(isFull);
|
||||||
|
return isFull;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or removes the `fullwidth=1` URL parameter.
|
||||||
|
* @param {boolean} enabled
|
||||||
|
*/
|
||||||
|
function updateUrlFullWidth(enabled) {
|
||||||
|
var url = new URL(window.location);
|
||||||
|
if (enabled) url.searchParams.set('fullwidth', '1');
|
||||||
|
else url.searchParams.delete('fullwidth');
|
||||||
|
window.history.replaceState({}, '', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose globally
|
||||||
|
window.setFullWidth = setFullWidth;
|
||||||
|
window.initFullWidthFromUrl = initFullWidthFromUrl;
|
||||||
|
window.updateUrlFullWidth = updateUrlFullWidth;
|
@@ -1,101 +1,158 @@
|
|||||||
// Global variables to store elements and original state
|
// Global variables to store elements and original state
|
||||||
let mainElement, originalContent, originalMainStyle, container, customScrollbar;
|
let mainElement, originalContent, originalMainStyle, container, customScrollbar, scrollbarContainer;
|
||||||
|
|
||||||
// Function to open a URL in an iframe using global variables
|
// Synchronize the height of the iframe to match the scroll-container or main element
|
||||||
|
function syncIframeHeight() {
|
||||||
|
const iframe = mainElement.querySelector("iframe");
|
||||||
|
if (iframe) {
|
||||||
|
console.log("Setting iframe height based on scroll-container inline styles...");
|
||||||
|
if (scrollbarContainer) {
|
||||||
|
// Prefer inline height, otherwise inline max-height
|
||||||
|
const inlineHeight = scrollbarContainer.style.height;
|
||||||
|
const inlineMax = scrollbarContainer.style.maxHeight;
|
||||||
|
const target = inlineHeight || inlineMax;
|
||||||
|
if (target) {
|
||||||
|
console.log("Using scroll-container inline style:", target);
|
||||||
|
iframe.style.height = target;
|
||||||
|
} else {
|
||||||
|
console.warn("No inline height or max-height set on scroll-container. Using main element height instead.");
|
||||||
|
iframe.style.height = mainElement.style.height;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No scroll-container found. Using main element height:", mainElement.style.height);
|
||||||
|
iframe.style.height = mainElement.style.height;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No iframe to resize.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to open a URL in an iframe
|
||||||
function openIframe(url) {
|
function openIframe(url) {
|
||||||
// Set a fixed height for the main element if not already set
|
// Hide the container (and its scroll-container) so the iframe can appear in its place
|
||||||
if (!mainElement.style.height) {
|
if (scrollbarContainer) {
|
||||||
mainElement.style.height = `${mainElement.clientHeight}px`;
|
scrollbarContainer.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace the container class with container-fluid if not already applied
|
// Hide any custom scrollbar element if present
|
||||||
if (container && !container.classList.contains("container-fluid")) {
|
|
||||||
container.classList.replace("container", "container-fluid");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide the custom scrollbar
|
|
||||||
if (customScrollbar) {
|
if (customScrollbar) {
|
||||||
customScrollbar.style.display = "none";
|
customScrollbar.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if an iframe already exists in the main element
|
// Create or retrieve the iframe in the main element
|
||||||
let iframe = mainElement.querySelector("iframe");
|
let iframe = mainElement.querySelector("iframe");
|
||||||
if (!iframe) {
|
if (!iframe) {
|
||||||
// Create a new iframe element
|
|
||||||
iframe = document.createElement("iframe");
|
iframe = document.createElement("iframe");
|
||||||
iframe.width = "100%";
|
iframe.width = "100%";
|
||||||
iframe.style.border = "none";
|
iframe.style.border = "none";
|
||||||
iframe.style.height = mainElement.style.height; // Apply fixed height
|
iframe.style.overflow = "auto"; // Enable internal scrolling
|
||||||
iframe.style.overflow = "auto"; // Enable internal scrollbar
|
iframe.scrolling = "auto";
|
||||||
iframe.scrolling = "auto"; // Ensure scrollability
|
|
||||||
|
|
||||||
// Clear the main content before appending the iframe
|
|
||||||
mainElement.innerHTML = "";
|
|
||||||
mainElement.appendChild(iframe);
|
mainElement.appendChild(iframe);
|
||||||
|
syncIframeHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the URL of the iframe
|
// Set the iframe's source URL
|
||||||
iframe.src = url;
|
iframe.src = url;
|
||||||
|
|
||||||
|
// Push the new URL state without reloading the page
|
||||||
|
const newUrl = new URL(window.location);
|
||||||
|
newUrl.searchParams.set('iframe', url);
|
||||||
|
window.history.pushState({ iframe: url }, '', newUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
// Function to restore the original content and show the container again
|
||||||
// Initialize global variables
|
function restoreOriginal() {
|
||||||
|
// Remove the iframe from the DOM
|
||||||
|
const iframe = mainElement.querySelector("iframe");
|
||||||
|
if (iframe) {
|
||||||
|
iframe.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the original container
|
||||||
|
if (scrollbarContainer) {
|
||||||
|
scrollbarContainer.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore any custom scrollbar
|
||||||
|
if (customScrollbar) {
|
||||||
|
customScrollbar.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the original inline style of the main element
|
||||||
|
if (originalMainStyle !== null) {
|
||||||
|
mainElement.setAttribute("style", originalMainStyle);
|
||||||
|
} else {
|
||||||
|
mainElement.removeAttribute("style");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the URL to remove the iframe parameter
|
||||||
|
const newUrl = new URL(window.location);
|
||||||
|
newUrl.searchParams.delete("iframe");
|
||||||
|
window.history.pushState({}, '', newUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize event listeners after DOM content is loaded
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// Cache references to elements and original state
|
||||||
mainElement = document.querySelector("main");
|
mainElement = document.querySelector("main");
|
||||||
originalContent = mainElement.innerHTML;
|
originalContent = mainElement.innerHTML;
|
||||||
originalMainStyle = mainElement.getAttribute("style"); // might be null if no inline style exists
|
originalMainStyle = mainElement.getAttribute("style"); // may be null
|
||||||
|
|
||||||
container = document.querySelector(".container");
|
container = document.querySelector(".container");
|
||||||
customScrollbar = document.getElementById("custom-scrollbar");
|
customScrollbar = document.getElementById("custom-scrollbar");
|
||||||
|
scrollbarContainer = container.querySelector(".scroll-container")
|
||||||
|
|
||||||
// Get all links that should open in an iframe
|
// Attach click handlers to links that should open in an iframe
|
||||||
const links = document.querySelectorAll(".iframe-link");
|
document.querySelectorAll(".iframe-link").forEach(link => {
|
||||||
|
link.addEventListener("click", function(event) {
|
||||||
// Add click event listener to each iframe link
|
event.preventDefault(); // prevent full page navigation
|
||||||
links.forEach(link => {
|
openIframe(this.href);
|
||||||
link.addEventListener("click", function (event) {
|
updateUrlFullWidth(true);
|
||||||
event.preventDefault(); // Prevent default link behavior
|
|
||||||
const url = this.getAttribute("href");
|
|
||||||
openIframe(url);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add click event listener to header h1 to restore the original main content and style
|
// Clicking the header's H1 will restore the original view
|
||||||
const headerH1 = document.querySelector("header h1");
|
const headerH1 = document.querySelector("header h1");
|
||||||
if (headerH1) {
|
if (headerH1) {
|
||||||
headerH1.style.cursor = "pointer";
|
headerH1.style.cursor = "pointer";
|
||||||
headerH1.addEventListener("click", function () {
|
headerH1.addEventListener("click", restoreOriginal);
|
||||||
// Restore the original content of the main element (removing the iframe)
|
|
||||||
mainElement.innerHTML = originalContent;
|
|
||||||
|
|
||||||
// Restore the original inline style of the main element
|
|
||||||
if (originalMainStyle !== null) {
|
|
||||||
mainElement.setAttribute("style", originalMainStyle);
|
|
||||||
} else {
|
|
||||||
mainElement.removeAttribute("style");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally revert the container class back to "container" if needed
|
|
||||||
if (container && container.classList.contains("container-fluid")) {
|
|
||||||
container.classList.replace("container-fluid", "container");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally show the custom scrollbar again
|
|
||||||
if (customScrollbar) {
|
|
||||||
customScrollbar.style.display = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust scroll container height if that function exists
|
|
||||||
if (typeof adjustScrollContainerHeight === "function") {
|
|
||||||
adjustScrollContainerHeight();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust iframe height on window resize (optional, to keep it responsive)
|
// On full page load, check URL parameters to auto-open an iframe
|
||||||
window.addEventListener("resize", function () {
|
window.addEventListener("load", function() {
|
||||||
const iframe = mainElement.querySelector("iframe");
|
const params = new URLSearchParams(window.location.search);
|
||||||
if (iframe) {
|
const iframeUrl = params.get('iframe');
|
||||||
iframe.style.height = mainElement.style.height;
|
if (iframeUrl) {
|
||||||
|
openIframe(iframeUrl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle browser back/forward navigation
|
||||||
|
window.addEventListener('popstate', function(event) {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const iframeUrl = params.get('iframe');
|
||||||
|
|
||||||
|
if (iframeUrl) {
|
||||||
|
openIframe(iframeUrl);
|
||||||
|
} else {
|
||||||
|
restoreOriginal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the current iframe URL in a new browser tab.
|
||||||
|
*/
|
||||||
|
function openIframeInNewTab() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const iframeUrl = params.get('iframe');
|
||||||
|
if (iframeUrl) {
|
||||||
|
window.open(iframeUrl, '_blank');
|
||||||
|
} else {
|
||||||
|
alert('No iframe is currently open.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// expose globally so your template’s onclick can find it
|
||||||
|
window.openIframeInNewTab = openIframeInNewTab;
|
||||||
|
|
||||||
|
// Adjust iframe height on window resize
|
||||||
|
window.addEventListener('resize', syncIframeHeight);
|
||||||
|
@@ -49,10 +49,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Include modal -->
|
<!-- Include modal -->
|
||||||
{% include "moduls/modal.html.j2" %}
|
{% include "moduls/modal.html.j2" %}
|
||||||
<script src="{{ url_for('static', filename='js/modal.js') }}"></script>
|
{% for name in [
|
||||||
<script src="{{ url_for('static', filename='js/navigation.js') }}"></script>
|
'modal',
|
||||||
<script src="{{ url_for('static', filename='js/tooltip.js') }}"></script>
|
'navigation',
|
||||||
<script src="{{ url_for('static', filename='js/custom_scrollbar.js') }}"></script>
|
'tooltip',
|
||||||
<script src="{{ url_for('static', filename='js/iframe.js') }}"></script>
|
'container',
|
||||||
|
'fullwidth',
|
||||||
|
'fullscreen',
|
||||||
|
'iframe',
|
||||||
|
] %}
|
||||||
|
<script src="{{ url_for('static', filename='js/' ~ name ~ '.js') }}"></script>
|
||||||
|
{% endfor %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@@ -8,65 +8,83 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
<!-- Template for children -->
|
<!-- Template for children -->
|
||||||
{% macro render_children(children) %}
|
{% macro render_children(children) %}
|
||||||
{% for children in children %}
|
{% for child in children %}
|
||||||
{% if children.children %}
|
{% if child.children %}
|
||||||
<li class="dropdown-submenu position-relative">
|
<li class="dropdown-submenu position-relative">
|
||||||
<a class="dropdown-item dropdown-toggle" title="{{ children.description }}">
|
<a class="dropdown-item dropdown-toggle" title="{{ child.description }}">
|
||||||
{{ render_icon_and_name(children) }}
|
{{ render_icon_and_name(child) }}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{{ render_children(children.children) }}
|
{{ render_children(child.children) }}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% elif children.identifier or children.warning or children.info %}
|
|
||||||
<li>
|
{% elif child.identifier or child.warning or child.info %}
|
||||||
<a class="dropdown-item" onclick='openDynamicPopup({{ children|tojson|safe }})' data-bs-toggle="tooltip" title="{{ children.description }}">
|
<li>
|
||||||
{{ render_icon_and_name(children) }}
|
<a class="dropdown-item"
|
||||||
</a>
|
onclick='openDynamicPopup({{ child|tojson|safe }})'
|
||||||
</li>
|
data-bs-toggle="tooltip"
|
||||||
{% else %}
|
title="{{ child.description }}">
|
||||||
<li>
|
{{ render_icon_and_name(child) }}
|
||||||
<a class="dropdown-item {% if children.iframe %}iframe-link{% endif %}" href="{{ children.url }}" target="{{ children.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ children.description }}">
|
</a>
|
||||||
{{ render_icon_and_name(children) }}
|
</li>
|
||||||
</a>
|
|
||||||
</li>
|
{% else %}
|
||||||
{% endif %}
|
<li>
|
||||||
{% endfor %}
|
<a class="dropdown-item {% if child.iframe %}iframe-link{% endif %}"
|
||||||
|
{% if child.onclick %}
|
||||||
|
onclick="{{ child.onclick }}"
|
||||||
|
{% else %}
|
||||||
|
href="{{ child.url }}"
|
||||||
|
{% endif %}
|
||||||
|
target="{{ child.target|default('_blank') }}"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="{{ child.description }}">
|
||||||
|
{{ render_icon_and_name(child) }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<!-- Navigation Bar -->
|
<!-- Navigation Bar -->
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light menu-{{menu_type}}">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light menu-{{menu_type}}">
|
||||||
<div class="container-fluid">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav{{menu_type}}" aria-controls="navbarNav{{menu_type}}" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav{{menu_type}}" aria-controls="navbarNav{{menu_type}}" aria-expanded="false" aria-label="Toggle navigation">
|
<span class="navbar-toggler-icon"></span>
|
||||||
<span class="navbar-toggler-icon"></span>
|
</button>
|
||||||
</button>
|
<div class="collapse navbar-collapse" id="navbarNav{{menu_type}}">
|
||||||
<div class="collapse navbar-collapse" id="navbarNav{{menu_type}}">
|
<ul class="navbar-nav {% if menu_type == 'header' %}ms-auto{% endif %} btn-group">
|
||||||
<ul class="navbar-nav {% if menu_type == 'header' %}ms-auto{% endif %} btn-group">
|
{% for item in navigation[menu_type].children %}
|
||||||
{% for item in navigation[menu_type].children %}
|
{% if item.url or item.onclick %}
|
||||||
{% if item.url %}
|
<li class="nav-item">
|
||||||
<!-- Single Item -->
|
<a class="nav-link btn btn-light {% if item.iframe %}iframe-link{% endif %}"
|
||||||
<li class="nav-item">
|
{% if item.onclick %}
|
||||||
<a class="nav-link btn btn-light {% if item.iframe %}iframe-link{% endif %}" href="{{ item.url }}" target="{{ item.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ item.description }}">
|
onclick="{{ item.onclick }}"
|
||||||
|
{% else %}
|
||||||
|
href="{{ item.url }}"
|
||||||
|
{% endif %}
|
||||||
|
target="{{ item.target|default('_blank') }}"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="{{ item.description }}">
|
||||||
|
{{ render_icon_and_name(item) }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<!-- Dropdown Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle btn btn-light" id="navbarDropdown{{ loop.index }}" role="button" data-bs-display="dynamic" aria-expanded="false">
|
||||||
|
{% if item.icon is defined and item.icon.class is defined %}
|
||||||
{{ render_icon_and_name(item) }}
|
{{ render_icon_and_name(item) }}
|
||||||
</a>
|
{% else %}
|
||||||
</li>
|
<p>Missing icon in item: {{ item }}</p>
|
||||||
{% else %}
|
{% endif %}
|
||||||
<!-- Dropdown Menu -->
|
</a>
|
||||||
<li class="nav-item dropdown">
|
<ul class="dropdown-menu">
|
||||||
<a class="nav-link dropdown-toggle btn btn-light" id="navbarDropdown{{ loop.index }}" role="button" data-bs-display="dynamic" aria-expanded="false">
|
{{ render_children(item.children) }}
|
||||||
{% if item.icon is defined and item.icon.class is defined %}
|
</ul>
|
||||||
{{ render_icon_and_name(item) }}
|
</li>
|
||||||
{% else %}
|
{% endif %}
|
||||||
<p>Missing icon in item: {{ item }}</p>
|
{% endfor %}
|
||||||
{% endif %}
|
</ul>
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
{{ render_children(item.children) }}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@@ -11,6 +11,8 @@ services:
|
|||||||
- "${PORT:-5000}:${PORT:-5000}"
|
- "${PORT:-5000}:${PORT:-5000}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./app:/app
|
- ./app:/app
|
||||||
|
- ./.env:/app./.env
|
||||||
environment:
|
environment:
|
||||||
- PORT=${PORT:-5000}
|
- PORT=${PORT:-5000}
|
||||||
|
- FLASK_ENV=${FLASK_ENV:-production}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
17
main.py
17
main.py
@@ -25,7 +25,6 @@ import os
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Always load .env from the script's directory
|
|
||||||
dotenv_path = Path(__file__).resolve().parent / ".env"
|
dotenv_path = Path(__file__).resolve().parent / ".env"
|
||||||
|
|
||||||
if dotenv_path.exists():
|
if dotenv_path.exists():
|
||||||
@@ -34,14 +33,14 @@ else:
|
|||||||
print(f"⚠️ Warning: No .env file found at {dotenv_path}")
|
print(f"⚠️ Warning: No .env file found at {dotenv_path}")
|
||||||
PORT = int(os.getenv("PORT", 5000))
|
PORT = int(os.getenv("PORT", 5000))
|
||||||
|
|
||||||
def run_command(command, dry_run=False):
|
def run_command(command, dry_run=False, env=None):
|
||||||
"""Utility function to run a shell command."""
|
"""Utility function to run a shell command."""
|
||||||
print(f"Executing: {' '.join(command)}")
|
print(f"Executing: {' '.join(command)}")
|
||||||
if dry_run:
|
if dry_run:
|
||||||
print("Dry run enabled: command not executed.")
|
print("Dry run enabled: command not executed.")
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(command)
|
subprocess.check_call(command, env=env)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f"Error: Command failed with exit code {e.returncode}")
|
print(f"Error: Command failed with exit code {e.returncode}")
|
||||||
sys.exit(e.returncode)
|
sys.exit(e.returncode)
|
||||||
@@ -126,7 +125,7 @@ def run_prod(args):
|
|||||||
"""
|
"""
|
||||||
command = [
|
command = [
|
||||||
"docker", "run", "-d",
|
"docker", "run", "-d",
|
||||||
"-p", "{PORT}:{PORT}",
|
"-p", "{PORT}:5000",
|
||||||
"--name", "portfolio",
|
"--name", "portfolio",
|
||||||
"application-portfolio"
|
"application-portfolio"
|
||||||
]
|
]
|
||||||
@@ -148,18 +147,12 @@ def logs(args):
|
|||||||
def dev(args):
|
def dev(args):
|
||||||
"""
|
"""
|
||||||
Run the application in development mode using docker-compose.
|
Run the application in development mode using docker-compose.
|
||||||
|
|
||||||
Command:
|
|
||||||
FLASK_ENV=development docker-compose up -d
|
|
||||||
|
|
||||||
This command sets the FLASK_ENV environment variable to 'development'
|
|
||||||
and starts the application using docker-compose, enabling hot-reloading.
|
|
||||||
"""
|
"""
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["FLASK_ENV"] = "development"
|
env["FLASK_ENV"] = "development"
|
||||||
command = ["docker-compose", "up", "-d"]
|
command = ["docker-compose", "up", "-d"]
|
||||||
print("Setting FLASK_ENV=development")
|
print("▶️ Starting in development mode (FLASK_ENV=development)")
|
||||||
run_command(command, args.dry_run)
|
run_command(command, args.dry_run, env=env)
|
||||||
|
|
||||||
def prod(args):
|
def prod(args):
|
||||||
"""
|
"""
|
||||||
|
Reference in New Issue
Block a user