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.title}

+
+ ${new Date(thought.created_at).toLocaleDateString('de-DE')} + ${thought.author ? `${thought.author.username}` : ''} +
+
+
+

${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); + } + }); });