diff --git a/app.py b/app.py
index 059ecd1..1786f63 100644
--- a/app.py
+++ b/app.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
import os
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, g
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_sqlalchemy import SQLAlchemy
@@ -292,7 +292,7 @@ def login():
if user and user.check_password(password):
login_user(user)
# Aktualisiere letzten Login-Zeitpunkt
- user.last_login = datetime.utcnow()
+ user.last_login = datetime.now(timezone.utc)
db.session.commit()
next_page = request.args.get('next')
@@ -708,7 +708,7 @@ def update_public_node(node_id):
node.color_code = data['color_code']
# Als bearbeitet markieren
- node.last_modified = datetime.utcnow()
+ node.last_modified = datetime.now(timezone.utc)
node.last_modified_by = current_user.id
db.session.commit()
@@ -1057,7 +1057,7 @@ def update_note(note_id):
if color_code:
note.color_code = color_code
- note.last_modified = datetime.utcnow()
+ note.last_modified = datetime.now(timezone.utc)
db.session.commit()
return jsonify({
@@ -1276,7 +1276,7 @@ def update_thought(thought_id):
if 'color_code' in data:
thought.color_code = data['color_code']
- thought.last_modified = datetime.utcnow()
+ thought.last_modified = datetime.now(timezone.utc)
db.session.commit()
return jsonify({
diff --git a/static/css/base-styles.css b/static/css/base-styles.css
index dc2a096..a318456 100644
--- a/static/css/base-styles.css
+++ b/static/css/base-styles.css
@@ -407,7 +407,7 @@ html.dark ::-webkit-scrollbar-thumb:hover {
}
.section-heading {
- font-size: 1.5rem;
+ font-size: 1.75rem;
}
}
@@ -819,4 +819,199 @@ body:not(.dark) .user-dropdown {
body:not(.dark) .user-dropdown-item:hover {
background-color: #f3f4f6;
+}
+
+/* Medienabfragen für Responsivität */
+@media (max-width: 640px) {
+ /* Optimierungen für Smartphones */
+ body {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .container {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+
+ .hero-heading {
+ font-size: 2rem;
+ }
+
+ .section-heading {
+ font-size: 1.75rem;
+ }
+
+ .card, .panel, .glass-card {
+ padding: 1rem;
+ border-radius: 10px;
+ }
+
+ .navbar {
+ padding: 0.75rem 1rem;
+ }
+
+ /* Optimierte Touch-Ziele für mobile Geräte */
+ button, .btn, .nav-link, .menu-item {
+ min-height: 44px;
+ min-width: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ /* Verbesserte Lesbarkeit auf kleinen Bildschirmen */
+ p, li, input, textarea, button, .text-sm {
+ font-size: 1rem;
+ line-height: 1.5;
+ }
+
+ /* Anpassungen für Tabellen auf kleinen Bildschirmen */
+ table {
+ display: block;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+
+ /* Optimierte Formulare */
+ input, select, textarea {
+ font-size: 16px; /* Verhindert iOS-Zoom bei Fokus */
+ width: 100%;
+ }
+
+ /* Verbesserter Abstand für Touch-Targets */
+ nav a, nav button, .menu-item {
+ margin: 0.25rem 0;
+ padding: 0.75rem;
+ }
+}
+
+@media (min-width: 641px) and (max-width: 1024px) {
+ /* Optimierungen für Tablets */
+ .container {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ }
+
+ /* Zweispaltige Layouts für mittlere Bildschirme */
+ .grid-cols-1 {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1.5rem;
+ }
+
+ /* Optimierte Navigationsleiste */
+ .navbar {
+ padding: 0.75rem 1.5rem;
+ }
+}
+
+@media (min-width: 1025px) {
+ /* Optimierungen für Desktop */
+ .container {
+ padding-left: 2rem;
+ padding-right: 2rem;
+ max-width: 1280px;
+ margin: 0 auto;
+ }
+
+ /* Mehrspaltige Layouts für große Bildschirme */
+ .grid-cols-1 {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 2rem;
+ }
+
+ /* Hover-Effekte nur auf Desktop-Geräten */
+ .card:hover, .panel:hover, .glass-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ }
+
+ /* Desktop-spezifische Animationen */
+ .animate-hover {
+ transition: all 0.3s ease;
+ }
+
+ .animate-hover:hover {
+ transform: translateY(-3px);
+ box-shadow: var(--shadow-lg);
+ }
+}
+
+/* Responsive design improvements */
+.responsive-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+}
+
+.responsive-flex {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.responsive-flex > * {
+ flex: 1 1 280px;
+}
+
+/* Accessibility improvements */
+.focus-visible:focus-visible {
+ outline: 2px solid var(--accent-primary-light);
+ outline-offset: 2px;
+}
+
+body.dark .focus-visible:focus-visible {
+ outline: 2px solid var(--accent-primary-dark);
+}
+
+/* Print styles */
+@media print {
+ body {
+ background: white !important;
+ color: black !important;
+ }
+
+ nav, footer, button, .no-print {
+ display: none !important;
+ }
+
+ main, article, .card, .panel, .container {
+ width: 100% !important;
+ border: none !important;
+ box-shadow: none !important;
+ color: black !important;
+ background: white !important;
+ }
+
+ a {
+ color: black !important;
+ text-decoration: underline !important;
+ }
+
+ a::after {
+ content: " (" attr(href) ")";
+ font-size: 0.8em;
+ font-weight: normal;
+ }
+
+ h1, h2, h3, h4, h5, h6 {
+ page-break-after: avoid;
+ page-break-inside: avoid;
+ }
+
+ img {
+ page-break-inside: avoid;
+ max-width: 100% !important;
+ }
+
+ table {
+ page-break-inside: avoid;
+ }
+
+ @page {
+ margin: 2cm;
+ }
}
\ No newline at end of file
diff --git a/static/js/mindmap-init.js b/static/js/mindmap-init.js
index 9968415..4dcccdc 100644
--- a/static/js/mindmap-init.js
+++ b/static/js/mindmap-init.js
@@ -1018,5 +1018,232 @@ class MindMap {
// Implementierung für das Erstellen von Verbindungen
}
- // ... existing code ...
+ // Zeigt Gedanken für einen Knoten an
+ showThoughtsForNode(node) {
+ const nodeId = node.id().split('_')[1]; // "node_123" => "123"
+
+ // Wir verwenden die fetchThoughtsForNode-Methode, die wir bereits implementiert haben
+ this.fetchThoughtsForNode(nodeId)
+ .then(thoughts => {
+ // Nur fortfahren, wenn wir tatsächlich Gedanken haben
+ if (thoughts.length === 0) {
+ this.showFlash('Keine Gedanken für diesen Knoten gefunden', 'info');
+ return;
+ }
+
+ // Erstelle einen Gedanken-Viewer, wenn er nicht existiert
+ let thoughtsViewer = document.getElementById('thoughts-viewer');
+ if (!thoughtsViewer) {
+ thoughtsViewer = document.createElement('div');
+ thoughtsViewer.id = 'thoughts-viewer';
+ thoughtsViewer.className = 'fixed inset-0 flex items-center justify-center z-50';
+ thoughtsViewer.innerHTML = `
+
+
+
+
Gedanken zu:
+
+
+
+
+ `;
+ document.body.appendChild(thoughtsViewer);
+
+ // Event-Listener für Schließen-Button
+ document.getElementById('close-thoughts').addEventListener('click', () => {
+ // Animation für das Schließen
+ const content = thoughtsViewer.querySelector('.thoughts-content');
+ const backdrop = thoughtsViewer.querySelector('.thoughts-backdrop');
+
+ content.style.transform = 'scale(0.9)';
+ content.style.opacity = '0';
+ backdrop.style.opacity = '0';
+
+ setTimeout(() => thoughtsViewer.remove(), 300);
+ });
+
+ // Event-Listener für Backdrop-Klick
+ thoughtsViewer.querySelector('.thoughts-backdrop').addEventListener('click', () => {
+ document.getElementById('close-thoughts').click();
+ });
+ }
+
+ // Aktualisiere den Titel mit dem Knotennamen
+ thoughtsViewer.querySelector('.node-name').textContent = node.data('name');
+
+ // Container für die Gedanken
+ const thoughtsContainer = thoughtsViewer.querySelector('.thoughts-container');
+ thoughtsContainer.innerHTML = '';
+
+ // Gedanken rendern
+ this.renderThoughts(thoughts, thoughtsContainer);
+
+ // Animation für das Öffnen
+ const content = thoughtsViewer.querySelector('.thoughts-content');
+ const backdrop = thoughtsViewer.querySelector('.thoughts-backdrop');
+
+ content.style.transform = 'scale(0.9)';
+ content.style.opacity = '0';
+ backdrop.style.opacity = '0';
+
+ setTimeout(() => {
+ content.style.transition = 'all 0.3s ease';
+ backdrop.style.transition = 'opacity 0.3s ease';
+ content.style.transform = 'scale(1)';
+ content.style.opacity = '1';
+ backdrop.style.opacity = '1';
+ }, 10);
+ });
+ }
+
+ // Rendert die Gedanken in der UI mit Animationen
+ renderThoughts(thoughts, container) {
+ if (!container) return;
+
+ container.innerHTML = '';
+
+ // Animation delay counter
+ let delay = 0;
+
+ thoughts.forEach(thought => {
+ const thoughtCard = document.createElement('div');
+ thoughtCard.className = 'thought-card mb-4 bg-slate-700/50 rounded-lg overflow-hidden border border-slate-600/50 transition-all duration-300 hover:border-purple-500/30';
+ thoughtCard.setAttribute('data-id', thought.id);
+ thoughtCard.style.opacity = '0';
+ thoughtCard.style.transform = 'translateY(20px)';
+
+ const cardColor = thought.color_code || this.colorPalette.default;
+
+ thoughtCard.innerHTML = `
+
+
+
${thought.abstract || thought.content.substring(0, 150) + '...'}
+
+
+ `;
+
+ // Animation-Effekt mit Verzögerung für jede Karte
+ setTimeout(() => {
+ thoughtCard.style.transition = 'all 0.5s ease';
+ thoughtCard.style.opacity = '1';
+ thoughtCard.style.transform = 'translateY(0)';
+ }, delay);
+ delay += 100; // Jede Karte erscheint mit 100ms Verzögerung
+
+ // Event-Listener für Klick auf Gedanken
+ thoughtCard.addEventListener('click', (e) => {
+ // Verhindern, dass der Link-Klick den Kartenklick auslöst
+ if (e.target.tagName === 'A' || e.target.closest('a')) return;
+ window.location.href = `/thoughts/${thought.id}`;
+ });
+
+ // Hover-Animation für Karten
+ thoughtCard.addEventListener('mouseenter', () => {
+ thoughtCard.style.transform = 'translateY(-5px)';
+ thoughtCard.style.boxShadow = '0 10px 25px rgba(0, 0, 0, 0.2)';
+ });
+
+ thoughtCard.addEventListener('mouseleave', () => {
+ thoughtCard.style.transform = 'translateY(0)';
+ thoughtCard.style.boxShadow = 'none';
+ });
+
+ container.appendChild(thoughtCard);
+ });
+ }
+
+ // Flash-Nachrichten mit Animationen
+ showFlash(message, type = 'info') {
+ if (!this.flashContainer) {
+ this.flashContainer = document.createElement('div');
+ this.flashContainer.className = 'fixed top-4 right-4 z-50 flex flex-col gap-2';
+ document.body.appendChild(this.flashContainer);
+ }
+
+ const flash = document.createElement('div');
+ flash.className = `flash-message p-3 rounded-lg shadow-lg flex items-center gap-3 max-w-xs text-white`;
+
+ // Verschiedene Stile je nach Typ
+ let backgroundColor, icon;
+ switch (type) {
+ case 'success':
+ backgroundColor = 'bg-green-500';
+ icon = 'fa-check-circle';
+ break;
+ case 'error':
+ backgroundColor = 'bg-red-500';
+ icon = 'fa-exclamation-circle';
+ break;
+ case 'warning':
+ backgroundColor = 'bg-yellow-500';
+ icon = 'fa-exclamation-triangle';
+ break;
+ default:
+ backgroundColor = 'bg-blue-500';
+ icon = 'fa-info-circle';
+ }
+
+ flash.classList.add(backgroundColor);
+
+ flash.innerHTML = `
+
+ ${message}
+
+ `;
+
+ // Füge den Flash zum Container hinzu
+ this.flashContainer.appendChild(flash);
+
+ // Einblend-Animation
+ flash.style.opacity = '0';
+ flash.style.transform = 'translateX(20px)';
+
+ setTimeout(() => {
+ flash.style.transition = 'all 0.3s ease';
+ flash.style.opacity = '1';
+ flash.style.transform = 'translateX(0)';
+ }, 10);
+
+ // Schließen-Button
+ const closeBtn = flash.querySelector('.flash-close');
+ closeBtn.addEventListener('click', () => {
+ // Ausblend-Animation
+ flash.style.opacity = '0';
+ flash.style.transform = 'translateX(20px)';
+
+ setTimeout(() => {
+ flash.remove();
+ }, 300);
+ });
+
+ // Automatisches Ausblenden nach 5 Sekunden
+ setTimeout(() => {
+ if (flash.parentNode) {
+ flash.style.opacity = '0';
+ flash.style.transform = 'translateX(20px)';
+
+ setTimeout(() => {
+ if (flash.parentNode) flash.remove();
+ }, 300);
+ }
+ }, 5000);
+
+ return flash;
+ }
}
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
index d3b5c78..353c47c 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -638,36 +638,66 @@
// Globaler Zugriff für externe Skripte
window.MindMap = window.MindMap || {};
- window.MindMap.toggleDarkMode = function() {
- // Alpine.js-Instanz benutzen, wenn verfügbar
+ // Funktion zum Anwenden des Dark Mode, strikt getrennt
+ function applyDarkModeClasses(isDarkMode) {
+ if (isDarkMode) {
+ document.documentElement.classList.add('dark');
+ document.body.classList.add('dark');
+ localStorage.setItem('colorMode', 'dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ document.body.classList.remove('dark');
+ localStorage.setItem('colorMode', 'light');
+ }
+
+ // Alpine.js darkMode-Variable aktualisieren, falls zutreffend
const appEl = document.querySelector('body');
if (appEl && appEl.__x) {
- appEl.__x.$data.toggleDarkMode();
- } else {
- // Fallback: Nur classList und localStorage
- const isDark = document.documentElement.classList.contains('dark');
- document.documentElement.classList.toggle('dark', !isDark);
- document.body.classList.toggle('dark', !isDark);
- localStorage.setItem('colorMode', !isDark ? 'dark' : 'light');
-
- // Server aktualisieren
- fetch('/api/set_dark_mode', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ darkMode: !isDark })
- }).catch(console.error);
+ appEl.__x.$data.darkMode = isDarkMode;
}
+ }
+
+ window.MindMap.toggleDarkMode = function() {
+ const isDark = document.body.classList.contains('dark');
+ applyDarkModeClasses(!isDark);
+
+ // Server aktualisieren
+ fetch('/api/set_dark_mode', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ darkMode: !isDark })
+ }).catch(console.error);
};
- // Fallback für Browser-Präferenz, falls keine Einstellung geladen werden konnte
+ // Initialisierung beim Laden
document.addEventListener('DOMContentLoaded', function() {
- if (!document.body.classList.contains('dark') && !document.documentElement.classList.contains('dark')) {
+ // Reihenfolge der Prüfungen: Serverseitige Einstellung > Lokale Einstellung > Browser-Präferenz
+
+ // 1. Zuerst lokale Einstellung prüfen
+ const storedMode = localStorage.getItem('colorMode');
+ if (storedMode) {
+ applyDarkModeClasses(storedMode === 'dark');
+ } else {
+ // 2. Fallback auf Browser-Präferenz
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
- if (prefersDark) {
- document.documentElement.classList.add('dark');
- document.body.classList.add('dark');
- }
+ applyDarkModeClasses(prefersDark);
}
+
+ // 3. Serverseitige Einstellung abrufen und anwenden
+ fetch('/api/get_dark_mode')
+ .then(response => response.json())
+ .then(data => {
+ const serverDarkMode = data.darkMode === true || data.darkMode === 'true';
+ applyDarkModeClasses(serverDarkMode);
+ })
+ .catch(error => console.error('Fehler beim Abrufen des Dark Mode Status:', error));
+
+ // Listener für Änderungen der Browser-Präferenz
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
+ if (localStorage.getItem('colorMode') === null) {
+ applyDarkModeClasses(e.matches);
+ }
+ });
});