Compare commits
12 Commits
505fb9aa47
...
9cc4e70cba
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cc4e70cba | |||
| a8cac08d30 | |||
| 42a7485ce1 | |||
| 54a5ccc224 | |||
| a99f82d4cf | |||
| 699127f41f | |||
| e8d356a27a | |||
| daf2704253 | |||
| 084059449f | |||
| c9bbc6ff25 | |||
| 742e3fda20 | |||
| 54aa246b79 |
Binary file not shown.
171
app.py
171
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
|
||||
@@ -278,7 +278,8 @@ def admin_required(f):
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
# Verwende session.get() anstelle von query.get() für SQLAlchemy 2.0 Kompatibilität
|
||||
return db.session.get(User, int(id))
|
||||
|
||||
# Routes for authentication
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
@@ -292,7 +293,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')
|
||||
@@ -628,16 +629,156 @@ def delete_mindmap(mindmap_id):
|
||||
# API-Endpunkte für Mindmap-Daten
|
||||
@app.route('/api/mindmap/public')
|
||||
def get_public_mindmap():
|
||||
"""Liefert die öffentliche Mindmap-Struktur."""
|
||||
# Hole alle Kategorien der obersten Ebene
|
||||
root_categories = Category.query.filter_by(parent_id=None).all()
|
||||
|
||||
# Baue Baumstruktur auf
|
||||
result = []
|
||||
for category in root_categories:
|
||||
result.append(build_category_tree(category))
|
||||
|
||||
return jsonify(result)
|
||||
"""Liefert die Standard-Mindmap-Struktur basierend auf Kategorien."""
|
||||
try:
|
||||
# Hole alle Hauptkategorien
|
||||
categories = Category.query.filter_by(parent_id=None).all()
|
||||
|
||||
# Transformiere zu einer Baumstruktur
|
||||
category_tree = [build_category_tree(cat) for cat in categories]
|
||||
|
||||
return jsonify(category_tree)
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Abrufen der Mindmap: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Mindmap konnte nicht geladen werden'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/mindmap/public/add_node', methods=['POST'])
|
||||
@login_required
|
||||
def add_node_to_public_mindmap():
|
||||
"""Fügt einen neuen Knoten zur öffentlichen Mindmap hinzu."""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
name = data.get('name')
|
||||
description = data.get('description', '')
|
||||
color_code = data.get('color_code', '#8b5cf6')
|
||||
x_position = data.get('x_position', 0)
|
||||
y_position = data.get('y_position', 0)
|
||||
|
||||
if not name:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Knotenname ist erforderlich'
|
||||
}), 400
|
||||
|
||||
# Neuen Knoten erstellen
|
||||
new_node = MindMapNode(
|
||||
name=name,
|
||||
description=description,
|
||||
color_code=color_code
|
||||
)
|
||||
|
||||
db.session.add(new_node)
|
||||
db.session.flush() # ID generieren
|
||||
|
||||
# Als Beitrag des aktuellen Benutzers markieren
|
||||
new_node.contributed_by = current_user.id
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'node_id': new_node.id,
|
||||
'message': 'Knoten erfolgreich hinzugefügt'
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Fehler beim Hinzufügen des Knotens: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Hinzufügen des Knotens: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/mindmap/public/update_node/<int:node_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_public_node(node_id):
|
||||
"""Aktualisiert einen Knoten in der öffentlichen Mindmap."""
|
||||
try:
|
||||
node = MindMapNode.query.get_or_404(node_id)
|
||||
data = request.json
|
||||
|
||||
# Aktualisiere Knotendaten
|
||||
if 'name' in data:
|
||||
node.name = data['name']
|
||||
if 'description' in data:
|
||||
node.description = data['description']
|
||||
if 'color_code' in data:
|
||||
node.color_code = data['color_code']
|
||||
|
||||
# Als bearbeitet markieren
|
||||
node.last_modified = datetime.now(timezone.utc)
|
||||
node.last_modified_by = current_user.id
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Knoten erfolgreich aktualisiert'
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Fehler beim Aktualisieren des Knotens: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Aktualisieren des Knotens: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/mindmap/public/remove_node/<int:node_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def remove_node_from_public_mindmap(node_id):
|
||||
"""Entfernt einen Knoten aus der öffentlichen Mindmap."""
|
||||
try:
|
||||
node = MindMapNode.query.get_or_404(node_id)
|
||||
|
||||
# Lösche den Knoten
|
||||
db.session.delete(node)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Knoten erfolgreich entfernt'
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Fehler beim Entfernen des Knotens: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Entfernen des Knotens: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/mindmap/public/update_layout', methods=['POST'])
|
||||
@login_required
|
||||
def update_public_layout():
|
||||
"""Aktualisiert die Positionen der Knoten in der öffentlichen Mindmap."""
|
||||
try:
|
||||
data = request.json
|
||||
positions = data.get('positions', [])
|
||||
|
||||
for pos in positions:
|
||||
node_id = pos.get('node_id')
|
||||
node = MindMapNode.query.get(node_id)
|
||||
|
||||
if node:
|
||||
# Position aktualisieren
|
||||
node.x_position = pos.get('x_position', 0)
|
||||
node.y_position = pos.get('y_position', 0)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Layout erfolgreich aktualisiert'
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Fehler beim Aktualisieren des Layouts: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Aktualisieren des Layouts: {str(e)}'
|
||||
}), 500
|
||||
|
||||
def build_category_tree(category):
|
||||
"""
|
||||
@@ -917,7 +1058,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({
|
||||
@@ -1136,7 +1277,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({
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||
#chatgpt-assistant {
|
||||
font-family: 'Inter', sans-serif;
|
||||
bottom: 4.5rem;
|
||||
}
|
||||
|
||||
#assistant-chat {
|
||||
@@ -10,6 +11,7 @@
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
#assistant-toggle {
|
||||
@@ -142,14 +144,21 @@
|
||||
.typing-indicator span {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: #888;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
opacity: 0.4;
|
||||
opacity: 0.6;
|
||||
animation: bounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
body.dark .typing-indicator span {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
body:not(.dark) .typing-indicator span {
|
||||
background-color: rgba(107, 114, 128, 0.8);
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@@ -173,11 +182,12 @@
|
||||
@media (max-width: 640px) {
|
||||
#assistant-chat {
|
||||
width: calc(100vw - 2rem) !important;
|
||||
max-height: 70vh !important;
|
||||
}
|
||||
|
||||
#chatgpt-assistant {
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
bottom: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,4 +210,27 @@ main {
|
||||
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Verbesserte Farbkontraste für Nachrichtenblasen */
|
||||
.user-message {
|
||||
background-color: rgba(124, 58, 237, 0.1) !important;
|
||||
color: #4B5563 !important;
|
||||
}
|
||||
|
||||
body.dark .user-message {
|
||||
background-color: rgba(124, 58, 237, 0.2) !important;
|
||||
color: #F9FAFB !important;
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
background-color: #F3F4F6 !important;
|
||||
color: #1F2937 !important;
|
||||
border-left: 3px solid #8B5CF6;
|
||||
}
|
||||
|
||||
body.dark .assistant-message {
|
||||
background-color: rgba(31, 41, 55, 0.5) !important;
|
||||
color: #F9FAFB !important;
|
||||
border-left: 3px solid #8B5CF6;
|
||||
}
|
||||
@@ -68,18 +68,37 @@ body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Dark Mode */
|
||||
html.dark body {
|
||||
/* Strikte Trennung: Dark Mode */
|
||||
html.dark body,
|
||||
body.dark {
|
||||
background-color: var(--bg-primary-dark);
|
||||
color: var(--text-primary-dark);
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
/* Strikte Trennung: Light Mode */
|
||||
html:not(.dark) body,
|
||||
body:not(.dark) {
|
||||
background-color: var(--light-bg);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
/* Verbesserte Trennung: Container und Karten */
|
||||
body.dark .card,
|
||||
body.dark .glass-card,
|
||||
body.dark .panel {
|
||||
background-color: var(--bg-secondary-dark);
|
||||
border-color: var(--border-dark);
|
||||
color: var(--text-primary-dark);
|
||||
}
|
||||
|
||||
body:not(.dark) .card,
|
||||
body:not(.dark) .glass-card,
|
||||
body:not(.dark) .panel {
|
||||
background-color: var(--light-card-bg);
|
||||
border-color: var(--light-border);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
@@ -388,7 +407,7 @@ html.dark ::-webkit-scrollbar-thumb:hover {
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,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;
|
||||
}
|
||||
}
|
||||
@@ -417,4 +417,833 @@ function getDefaultStyles(darkMode = document.documentElement.classList.contains
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
class MindMap {
|
||||
constructor(selector, options = {}) {
|
||||
// Standardoptionen festlegen
|
||||
this.options = Object.assign({
|
||||
// Standard-Basisoptionen
|
||||
editable: false, // Ist die Mindmap editierbar?
|
||||
isUserLoggedIn: false, // Ist der Benutzer angemeldet?
|
||||
isPublicMap: true, // Handelt es sich um die öffentliche Mindmap?
|
||||
initialZoom: 1, // Anfängliche Zoom-Stufe
|
||||
fitViewOnInit: true, // Passt die Ansicht automatisch an
|
||||
minZoom: 0.2, // Minimale Zoom-Stufe
|
||||
maxZoom: 3, // Maximale Zoom-Stufe
|
||||
// Farbpalette für verschiedene Elemente
|
||||
colorPalette: {
|
||||
node: {
|
||||
default: '#8b5cf6', // Standard-Knotenfarbe
|
||||
selected: '#7c3aed', // Farbe für ausgewählte Knoten
|
||||
hover: '#6d28d9' // Farbe für Hover-Effekt
|
||||
},
|
||||
edge: {
|
||||
default: '#8b5cf6', // Standard-Kantenfarbe
|
||||
selected: '#7c3aed', // Farbe für ausgewählte Kanten
|
||||
hover: '#6d28d9' // Farbe für Hover-Effekt
|
||||
},
|
||||
text: {
|
||||
default: '#f3f4f6', // Standard-Textfarbe
|
||||
dark: '#1f2937', // Dunkle Textfarbe (für helle Hintergründe)
|
||||
light: '#f9fafb' // Helle Textfarbe (für dunkle Hintergründe)
|
||||
},
|
||||
background: {
|
||||
dark: '#111827', // Dunkler Hintergrund
|
||||
light: '#f9fafb' // Heller Hintergrund
|
||||
}
|
||||
},
|
||||
// Anpassbare Funktionen
|
||||
callbacks: {
|
||||
onNodeClick: null, // Callback für Knotenklick
|
||||
onEdgeClick: null, // Callback für Kantenklick
|
||||
onViewportChange: null, // Callback für Änderung der Ansicht
|
||||
onSelectionChange: null, // Callback für Änderung der Auswahl
|
||||
onLoad: null // Callback nach dem Laden der Mindmap
|
||||
}
|
||||
}, options);
|
||||
|
||||
// Container-Element
|
||||
this.container = document.querySelector(selector);
|
||||
if (!this.container) {
|
||||
console.error(`Container mit Selektor '${selector}' nicht gefunden`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfen, ob der Benutzer angemeldet ist
|
||||
this.isUserLoggedIn = document.body.classList.contains('user-logged-in') || this.options.isUserLoggedIn;
|
||||
|
||||
// Wenn der Benutzer angemeldet ist und es sich um die öffentliche Mindmap handelt,
|
||||
// wird die Editierbarkeit aktiviert
|
||||
if (this.isUserLoggedIn && this.options.isPublicMap) {
|
||||
this.options.editable = true;
|
||||
}
|
||||
|
||||
// Initialisiere Cytoscape
|
||||
this.initCytoscape();
|
||||
|
||||
// Füge Bearbeiten-Funktionalität hinzu, wenn editierbar
|
||||
if (this.options.editable) {
|
||||
this.initEditableMode();
|
||||
}
|
||||
|
||||
// Lade Daten basierend auf dem Typ der Mindmap
|
||||
if (this.options.isPublicMap) {
|
||||
this.loadPublicMindmap();
|
||||
} else if (this.options.userMindmapId) {
|
||||
this.loadUserMindmap(this.options.userMindmapId);
|
||||
}
|
||||
|
||||
// Initialisiere UI-Elemente
|
||||
this.initUI();
|
||||
|
||||
// Event-Listener-Initialisierung
|
||||
this.initEventListeners();
|
||||
|
||||
// Flash-Message-System
|
||||
this.flashContainer = null;
|
||||
this.initFlashMessages();
|
||||
}
|
||||
|
||||
// ... existing code ...
|
||||
|
||||
// Initialisiert die Bearbeitungsmodi für die Mindmap
|
||||
initEditableMode() {
|
||||
// Toolbar für Bearbeitungsmodus hinzufügen
|
||||
this.addEditToolbar();
|
||||
|
||||
// Kontextmenü-Funktionalität für Knoten
|
||||
this.cy.on('cxttap', 'node', (evt) => {
|
||||
const node = evt.target;
|
||||
const position = evt.renderedPosition;
|
||||
|
||||
// Zeige Kontextmenü
|
||||
this.showNodeContextMenu(node, position);
|
||||
});
|
||||
|
||||
// Doppelklick-Funktion für das Hinzufügen neuer Knoten
|
||||
this.cy.on('dblclick', (evt) => {
|
||||
// Nur auf Hintergrund reagieren, nicht auf Knoten
|
||||
if (evt.target === this.cy) {
|
||||
const position = evt.position;
|
||||
this.showAddNodeDialog(position);
|
||||
}
|
||||
});
|
||||
|
||||
// Kontextmenü-Schließen bei Klick außerhalb
|
||||
document.addEventListener('click', (e) => {
|
||||
const contextMenu = document.getElementById('context-menu');
|
||||
if (contextMenu && !contextMenu.contains(e.target)) {
|
||||
contextMenu.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Zeigt ein Kontextmenü für einen Knoten an
|
||||
showNodeContextMenu(node, position) {
|
||||
// Entferne vorhandenes Kontextmenü
|
||||
const existingMenu = document.getElementById('context-menu');
|
||||
if (existingMenu) existingMenu.remove();
|
||||
|
||||
// Erstelle neues Kontextmenü
|
||||
const contextMenu = document.createElement('div');
|
||||
contextMenu.id = 'context-menu';
|
||||
contextMenu.style.position = 'absolute';
|
||||
contextMenu.style.left = `${position.x}px`;
|
||||
contextMenu.style.top = `${position.y}px`;
|
||||
contextMenu.style.zIndex = '1000';
|
||||
|
||||
// Menü-Elemente
|
||||
const menuItems = [
|
||||
{ label: 'Gedanken anzeigen', icon: 'fas fa-brain', action: () => this.showThoughtsForNode(node) },
|
||||
{ label: 'Neuen Gedanken erstellen', icon: 'fas fa-plus-circle', action: () => this.createThoughtForNode(node) },
|
||||
{ label: 'Notiz hinzufügen', icon: 'fas fa-sticky-note', action: () => this.addNoteToNode(node) },
|
||||
{ label: 'Knoten bearbeiten', icon: 'fas fa-edit', action: () => this.editNode(node) },
|
||||
{ label: 'Verbindung erstellen', icon: 'fas fa-link', action: () => this.startEdgeCreation(node) },
|
||||
{ label: 'Aus Mindmap entfernen', icon: 'fas fa-trash-alt', action: () => this.removeNodeFromMap(node) }
|
||||
];
|
||||
|
||||
// Menü erstellen
|
||||
menuItems.forEach(item => {
|
||||
const menuItem = document.createElement('div');
|
||||
menuItem.className = 'menu-item';
|
||||
menuItem.innerHTML = `<i class="${item.icon} mr-2"></i> ${item.label}`;
|
||||
menuItem.addEventListener('click', () => {
|
||||
item.action();
|
||||
contextMenu.remove();
|
||||
});
|
||||
contextMenu.appendChild(menuItem);
|
||||
});
|
||||
|
||||
// Anhängen an Body (außerhalb des Cytoscape-Containers)
|
||||
document.body.appendChild(contextMenu);
|
||||
|
||||
// Stellen Sie sicher, dass das Kontextmenü vollständig sichtbar ist
|
||||
const menuRect = contextMenu.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if (menuRect.right > windowWidth) {
|
||||
contextMenu.style.left = `${position.x - menuRect.width}px`;
|
||||
}
|
||||
|
||||
if (menuRect.bottom > windowHeight) {
|
||||
contextMenu.style.top = `${position.y - menuRect.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
// Fügt die Edit-Toolbar zur Mindmap hinzu
|
||||
addEditToolbar() {
|
||||
const toolbar = document.querySelector('.mindmap-toolbar');
|
||||
if (!toolbar) return;
|
||||
|
||||
// Trennlinie
|
||||
const separator = document.createElement('div');
|
||||
separator.className = 'border-l h-6 mx-2 opacity-20';
|
||||
toolbar.appendChild(separator);
|
||||
|
||||
// Bearbeiten-Buttons
|
||||
const editButtons = [
|
||||
{ id: 'add-node-btn', icon: 'fa-plus', text: 'Knoten hinzufügen', action: () => this.showAddNodeDialog() },
|
||||
{ id: 'save-layout-btn', icon: 'fa-save', text: 'Layout speichern', action: () => this.saveCurrentLayout() }
|
||||
];
|
||||
|
||||
editButtons.forEach(btn => {
|
||||
const button = document.createElement('button');
|
||||
button.id = btn.id;
|
||||
button.className = 'mindmap-action-btn edit-btn';
|
||||
button.innerHTML = `<i class="fas ${btn.icon}"></i><span>${btn.text}</span>`;
|
||||
button.addEventListener('click', btn.action);
|
||||
toolbar.appendChild(button);
|
||||
});
|
||||
|
||||
// Animation für die Edit-Buttons
|
||||
const editBtns = document.querySelectorAll('.edit-btn');
|
||||
editBtns.forEach(btn => {
|
||||
btn.style.opacity = '0';
|
||||
btn.style.transform = 'translateY(10px)';
|
||||
|
||||
setTimeout(() => {
|
||||
btn.style.transition = 'all 0.3s ease';
|
||||
btn.style.opacity = '1';
|
||||
btn.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// Zeigt den Dialog zum Hinzufügen eines neuen Knotens
|
||||
showAddNodeDialog(position = null) {
|
||||
// Entferne bestehende Dialoge
|
||||
const existingDialog = document.getElementById('add-node-dialog');
|
||||
if (existingDialog) existingDialog.remove();
|
||||
|
||||
// Erstelle Dialog
|
||||
const dialog = document.createElement('div');
|
||||
dialog.id = 'add-node-dialog';
|
||||
dialog.className = 'fixed inset-0 flex items-center justify-center z-50';
|
||||
dialog.innerHTML = `
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50"></div>
|
||||
<div class="bg-slate-800 rounded-lg p-6 w-full max-w-md relative z-10 border border-purple-500/20">
|
||||
<h3 class="text-xl font-semibold mb-4 text-white">Neuen Knoten hinzufügen</h3>
|
||||
<form id="add-node-form">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
||||
<input type="text" id="node-name" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-white">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">Beschreibung</label>
|
||||
<textarea id="node-description" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-white resize-none h-24"></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">Farbe</label>
|
||||
<input type="color" id="node-color" class="w-full h-10 border border-slate-700 rounded-md bg-slate-900" value="#8B5CF6">
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button type="button" id="cancel-add-node" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-md transition-colors">Abbrechen</button>
|
||||
<button type="submit" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-md transition-colors">Hinzufügen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Füge Dialog zum Body hinzu
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
// Animation für den Dialog
|
||||
const dialogContent = dialog.querySelector('.bg-slate-800');
|
||||
dialogContent.style.opacity = '0';
|
||||
dialogContent.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => {
|
||||
dialogContent.style.transition = 'all 0.3s ease';
|
||||
dialogContent.style.opacity = '1';
|
||||
dialogContent.style.transform = 'scale(1)';
|
||||
}, 10);
|
||||
|
||||
// Event-Listener für Abbrechen
|
||||
document.getElementById('cancel-add-node').addEventListener('click', () => {
|
||||
// Animation für das Schließen
|
||||
dialogContent.style.opacity = '0';
|
||||
dialogContent.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => dialog.remove(), 300);
|
||||
});
|
||||
|
||||
// Event-Listener für Overlay-Klick
|
||||
dialog.querySelector('.fixed.inset-0.bg-black').addEventListener('click', () => {
|
||||
// Animation für das Schließen
|
||||
dialogContent.style.opacity = '0';
|
||||
dialogContent.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => dialog.remove(), 300);
|
||||
});
|
||||
|
||||
// Event-Listener für Formular
|
||||
document.getElementById('add-node-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.getElementById('node-name').value;
|
||||
const description = document.getElementById('node-description').value;
|
||||
const color = document.getElementById('node-color').value;
|
||||
|
||||
if (!name.trim()) {
|
||||
this.showFlash('Bitte geben Sie einen Namen für den Knoten ein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Knoten hinzufügen
|
||||
this.addNodeToMindmap({
|
||||
name,
|
||||
description,
|
||||
color_code: color,
|
||||
position
|
||||
});
|
||||
|
||||
// Dialog schließen
|
||||
dialogContent.style.opacity = '0';
|
||||
dialogContent.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => dialog.remove(), 300);
|
||||
});
|
||||
|
||||
// Fokus auf das Name-Feld
|
||||
setTimeout(() => document.getElementById('node-name').focus(), 100);
|
||||
}
|
||||
|
||||
// Fügt einen neuen Knoten zur Mindmap hinzu
|
||||
addNodeToMindmap(nodeData) {
|
||||
// API-Anfrage vorbereiten
|
||||
const data = {
|
||||
name: nodeData.name,
|
||||
description: nodeData.description || '',
|
||||
color_code: nodeData.color_code || '#8b5cf6'
|
||||
};
|
||||
|
||||
// Position hinzufügen, falls vorhanden
|
||||
if (nodeData.position) {
|
||||
data.x_position = nodeData.position.x;
|
||||
data.y_position = nodeData.position.y;
|
||||
}
|
||||
|
||||
// API-Anfrage zum Hinzufügen des Knotens
|
||||
fetch('/api/mindmap/public/add_node', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerkantwort war nicht ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Hinzufügen des neuen Knotens zum Graphen
|
||||
this.cy.add({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: `node_${data.node_id}`,
|
||||
name: nodeData.name,
|
||||
description: nodeData.description || '',
|
||||
color: nodeData.color_code
|
||||
},
|
||||
position: nodeData.position || { x: 0, y: 0 }
|
||||
});
|
||||
|
||||
// Flash-Nachricht anzeigen
|
||||
this.showFlash('Knoten erfolgreich hinzugefügt', 'success');
|
||||
|
||||
// Ansicht anpassen
|
||||
this.cy.fit();
|
||||
} else {
|
||||
this.showFlash('Fehler beim Hinzufügen des Knotens: ' + (data.error || 'Unbekannter Fehler'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Hinzufügen des Knotens:', error);
|
||||
this.showFlash('Fehler beim Hinzufügen des Knotens', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Entfernt einen Knoten aus der Mindmap
|
||||
removeNodeFromMap(node) {
|
||||
if (!confirm('Möchten Sie diesen Knoten wirklich aus der Mindmap entfernen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeId = node.id().split('_')[1]; // "node_123" => "123"
|
||||
|
||||
// API-Anfrage zum Entfernen des Knotens
|
||||
fetch(`/api/mindmap/public/remove_node/${nodeId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerkantwort war nicht ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Knoten aus dem Graphen entfernen
|
||||
this.cy.remove(node);
|
||||
|
||||
// Flash-Nachricht anzeigen
|
||||
this.showFlash('Knoten erfolgreich entfernt', 'success');
|
||||
} else {
|
||||
this.showFlash('Fehler beim Entfernen des Knotens: ' + (data.error || 'Unbekannter Fehler'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Entfernen des Knotens:', error);
|
||||
this.showFlash('Fehler beim Entfernen des Knotens', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Speichert das aktuelle Layout der Mindmap
|
||||
saveCurrentLayout() {
|
||||
// Sammle alle Knotenpositionen
|
||||
const nodePositions = [];
|
||||
this.cy.nodes().forEach(node => {
|
||||
const idParts = node.id().split('_');
|
||||
if (idParts.length === 2 && idParts[0] === 'node') {
|
||||
nodePositions.push({
|
||||
node_id: parseInt(idParts[1]),
|
||||
x_position: node.position('x'),
|
||||
y_position: node.position('y')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API-Anfrage zum Speichern des Layouts
|
||||
fetch('/api/mindmap/public/update_layout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ positions: nodePositions })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerkantwort war nicht ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.showFlash('Layout erfolgreich gespeichert', 'success');
|
||||
} else {
|
||||
this.showFlash('Fehler beim Speichern des Layouts: ' + (data.error || 'Unbekannter Fehler'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Speichern des Layouts:', error);
|
||||
this.showFlash('Fehler beim Speichern des Layouts', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Zeigt den Dialog zum Bearbeiten eines Knotens
|
||||
editNode(node) {
|
||||
// Entferne bestehende Dialoge
|
||||
const existingDialog = document.getElementById('edit-node-dialog');
|
||||
if (existingDialog) existingDialog.remove();
|
||||
|
||||
// Knotendaten holen
|
||||
const nodeData = node.data();
|
||||
|
||||
// Erstelle Dialog
|
||||
const dialog = document.createElement('div');
|
||||
dialog.id = 'edit-node-dialog';
|
||||
dialog.className = 'fixed inset-0 flex items-center justify-center z-50';
|
||||
dialog.innerHTML = `
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50"></div>
|
||||
<div class="bg-slate-800 rounded-lg p-6 w-full max-w-md relative z-10 border border-purple-500/20">
|
||||
<h3 class="text-xl font-semibold mb-4 text-white">Knoten bearbeiten</h3>
|
||||
<form id="edit-node-form">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
||||
<input type="text" id="node-name" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-white" value="${nodeData.name}">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">Beschreibung</label>
|
||||
<textarea id="node-description" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-white resize-none h-24">${nodeData.description || ''}</textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">Farbe</label>
|
||||
<input type="color" id="node-color" class="w-full h-10 border border-slate-700 rounded-md bg-slate-900" value="${nodeData.color || '#8B5CF6'}">
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button type="button" id="cancel-edit-node" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-md transition-colors">Abbrechen</button>
|
||||
<button type="submit" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-md transition-colors">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Füge Dialog zum Body hinzu
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
// Animation für den Dialog
|
||||
const dialogContent = dialog.querySelector('.bg-slate-800');
|
||||
dialogContent.style.opacity = '0';
|
||||
dialogContent.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => {
|
||||
dialogContent.style.transition = 'all 0.3s ease';
|
||||
dialogContent.style.opacity = '1';
|
||||
dialogContent.style.transform = 'scale(1)';
|
||||
}, 10);
|
||||
|
||||
// Event-Listener für Abbrechen
|
||||
document.getElementById('cancel-edit-node').addEventListener('click', () => {
|
||||
// Animation für das Schließen
|
||||
dialogContent.style.opacity = '0';
|
||||
dialogContent.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => dialog.remove(), 300);
|
||||
});
|
||||
|
||||
// Event-Listener für Overlay-Klick
|
||||
dialog.querySelector('.fixed.inset-0.bg-black').addEventListener('click', () => {
|
||||
// Animation für das Schließen
|
||||
dialogContent.style.opacity = '0';
|
||||
dialogContent.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => dialog.remove(), 300);
|
||||
});
|
||||
|
||||
// Event-Listener für Formular
|
||||
document.getElementById('edit-node-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.getElementById('node-name').value;
|
||||
const description = document.getElementById('node-description').value;
|
||||
const color = document.getElementById('node-color').value;
|
||||
|
||||
if (!name.trim()) {
|
||||
this.showFlash('Bitte geben Sie einen Namen für den Knoten ein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Knoten aktualisieren
|
||||
this.updateNode(node, {
|
||||
name,
|
||||
description,
|
||||
color_code: color
|
||||
});
|
||||
|
||||
// Dialog schließen
|
||||
dialogContent.style.opacity = '0';
|
||||
dialogContent.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => dialog.remove(), 300);
|
||||
});
|
||||
|
||||
// Fokus auf das Name-Feld
|
||||
setTimeout(() => document.getElementById('node-name').focus(), 100);
|
||||
}
|
||||
|
||||
// Aktualisiert einen Knoten in der Mindmap
|
||||
updateNode(node, nodeData) {
|
||||
const nodeId = node.id().split('_')[1]; // "node_123" => "123"
|
||||
|
||||
// API-Anfrage zum Aktualisieren des Knotens
|
||||
fetch(`/api/mindmap/public/update_node/${nodeId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: nodeData.name,
|
||||
description: nodeData.description,
|
||||
color_code: nodeData.color_code
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerkantwort war nicht ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Aktualisiere Knotendaten im Graph
|
||||
node.data('name', nodeData.name);
|
||||
node.data('description', nodeData.description);
|
||||
node.data('color', nodeData.color_code);
|
||||
|
||||
// Aktualisiere Darstellung
|
||||
this.cy.style()
|
||||
.selector(`#${node.id()}`)
|
||||
.style({
|
||||
'background-color': nodeData.color_code
|
||||
})
|
||||
.update();
|
||||
|
||||
// Flash-Nachricht anzeigen
|
||||
this.showFlash('Knoten erfolgreich aktualisiert', 'success');
|
||||
} else {
|
||||
this.showFlash('Fehler beim Aktualisieren des Knotens: ' + (data.error || 'Unbekannter Fehler'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Aktualisieren des Knotens:', error);
|
||||
this.showFlash('Fehler beim Aktualisieren des Knotens', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Fügt eine Notiz zu einem Knoten hinzu
|
||||
addNoteToNode(node) {
|
||||
// Implementierung für das Hinzufügen von Notizen
|
||||
// Ähnlich wie bei editNode, aber mit anderen Feldern
|
||||
}
|
||||
|
||||
// Startet den Prozess zum Erstellen einer Verbindung
|
||||
startEdgeCreation(node) {
|
||||
// Implementierung für das Erstellen von Verbindungen
|
||||
}
|
||||
|
||||
// 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 = `
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 thoughts-backdrop"></div>
|
||||
<div class="bg-slate-800 rounded-lg w-full max-w-3xl mx-4 relative z-10 border border-purple-500/20 thoughts-content overflow-hidden">
|
||||
<div class="flex justify-between items-center p-4 border-b border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-white">Gedanken zu: <span class="node-name"></span></h3>
|
||||
<button id="close-thoughts" class="text-gray-400 hover:text-white">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4 thoughts-container max-h-[70vh] overflow-y-auto"></div>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="thought-card-header p-4" style="border-left: 4px solid ${cardColor}">
|
||||
<h3 class="thought-title text-lg font-semibold text-white">${thought.title}</h3>
|
||||
<div class="thought-meta flex gap-3 text-sm text-gray-400 mt-1">
|
||||
<span class="thought-date"><i class="far fa-calendar-alt mr-1"></i>${new Date(thought.created_at).toLocaleDateString('de-DE')}</span>
|
||||
${thought.author ? `<span class="thought-author"><i class="far fa-user mr-1"></i>${thought.author.username}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="thought-content p-4 text-gray-200">
|
||||
<p>${thought.abstract || thought.content.substring(0, 150) + '...'}</p>
|
||||
</div>
|
||||
<div class="thought-footer p-4 pt-0 flex justify-between items-center">
|
||||
<div class="thought-keywords flex flex-wrap gap-1">
|
||||
${thought.keywords ? thought.keywords.split(',').map(kw =>
|
||||
`<span class="keyword text-xs px-2 py-1 bg-purple-800/30 text-purple-200 rounded-full">${kw.trim()}</span>`).join('') : ''}
|
||||
</div>
|
||||
<a href="/thoughts/${thought.id}" class="thought-link text-purple-400 hover:text-purple-300 text-sm flex items-center">
|
||||
Mehr lesen <i class="fas fa-arrow-right ml-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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 = `
|
||||
<div class="flash-icon"><i class="fas ${icon} text-lg"></i></div>
|
||||
<div class="flash-text">${message}</div>
|
||||
<button class="flash-close ml-auto text-white/80 hover:text-white"><i class="fas fa-times"></i></button>
|
||||
`;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -247,130 +247,63 @@ class ChatGPTAssistant {
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = sender === 'user'
|
||||
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
||||
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
||||
|
||||
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
|
||||
let formattedText = '';
|
||||
|
||||
if (sender === 'assistant' && this.markdownParser) {
|
||||
// Für Assistentnachrichten Markdown verwenden
|
||||
try {
|
||||
formattedText = this.markdownParser.parse(text);
|
||||
|
||||
// CSS für Markdown-Formatierung hinzufügen
|
||||
const markdownStyles = `
|
||||
.markdown-bubble h1, .markdown-bubble h2, .markdown-bubble h3,
|
||||
.markdown-bubble h4, .markdown-bubble h5, .markdown-bubble h6 {
|
||||
font-weight: bold;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.markdown-bubble h1 { font-size: 1.4rem; }
|
||||
.markdown-bubble h2 { font-size: 1.3rem; }
|
||||
.markdown-bubble h3 { font-size: 1.2rem; }
|
||||
.markdown-bubble h4 { font-size: 1.1rem; }
|
||||
.markdown-bubble ul, .markdown-bubble ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.markdown-bubble ul { list-style-type: disc; }
|
||||
.markdown-bubble ol { list-style-type: decimal; }
|
||||
.markdown-bubble p { margin: 0.5rem 0; }
|
||||
.markdown-bubble code {
|
||||
font-family: monospace;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.markdown-bubble pre {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.markdown-bubble pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown-bubble blockquote {
|
||||
border-left: 3px solid rgba(0, 0, 0, 0.2);
|
||||
padding-left: 0.8rem;
|
||||
margin: 0.5rem 0;
|
||||
font-style: italic;
|
||||
}
|
||||
.dark .markdown-bubble code {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.dark .markdown-bubble pre {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.dark .markdown-bubble blockquote {
|
||||
border-left-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
// Füge die Styles hinzu, wenn sie noch nicht vorhanden sind
|
||||
if (!document.querySelector('#markdown-chat-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'markdown-chat-styles';
|
||||
style.textContent = markdownStyles;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Klasse für Markdown-Formatierung hinzufügen
|
||||
bubble.classList.add('markdown-bubble');
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Markdown-Formatierung:', error);
|
||||
// Fallback zur einfachen Formatierung
|
||||
formattedText = text.split('\n').map(line => {
|
||||
if (line.trim() === '') return '<br>';
|
||||
return `<p>${line}</p>`;
|
||||
}).join('');
|
||||
}
|
||||
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
|
||||
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||
|
||||
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||
if (this.markdownParser) {
|
||||
bubble.innerHTML = this.markdownParser.parse(text);
|
||||
} else {
|
||||
// Für Benutzernachrichten einfache Formatierung
|
||||
formattedText = text.split('\n').map(line => {
|
||||
if (line.trim() === '') return '<br>';
|
||||
return `<p>${line}</p>`;
|
||||
}).join('');
|
||||
bubble.textContent = text;
|
||||
}
|
||||
|
||||
bubble.innerHTML = formattedText;
|
||||
// Links in der Nachricht klickbar machen
|
||||
const links = bubble.querySelectorAll('a');
|
||||
links.forEach(link => {
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
link.className = 'text-primary-600 dark:text-primary-400 underline';
|
||||
});
|
||||
|
||||
// Code-Blöcke stylen
|
||||
const codeBlocks = bubble.querySelectorAll('pre');
|
||||
codeBlocks.forEach(block => {
|
||||
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
|
||||
});
|
||||
|
||||
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
|
||||
inlineCode.forEach(code => {
|
||||
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
|
||||
});
|
||||
|
||||
messageEl.appendChild(bubble);
|
||||
this.chatHistory.appendChild(messageEl);
|
||||
|
||||
if (this.chatHistory) {
|
||||
this.chatHistory.appendChild(messageEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
}
|
||||
// Scrolle zum Ende des Chat-Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Vorschläge als klickbare Pills an
|
||||
* @param {string[]} suggestions - Liste von Vorschlägen
|
||||
* Zeigt Vorschläge für mögliche Fragen an
|
||||
* @param {Array} suggestions - Array von Vorschlägen
|
||||
*/
|
||||
showSuggestions(suggestions) {
|
||||
if (!this.suggestionArea) return;
|
||||
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
|
||||
|
||||
// Vorherige Vorschläge entfernen
|
||||
this.suggestionArea.innerHTML = '';
|
||||
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
suggestions.forEach(suggestion => {
|
||||
const pill = document.createElement('button');
|
||||
pill.className = 'suggestion-pill text-sm bg-gray-200 dark:bg-dark-600 hover:bg-gray-300 dark:hover:bg-dark-500 text-gray-800 dark:text-gray-200 rounded-full px-3 py-1 mb-2 transition-colors';
|
||||
pill.textContent = suggestion;
|
||||
this.suggestionArea.appendChild(pill);
|
||||
});
|
||||
|
||||
this.suggestionArea.classList.remove('hidden');
|
||||
} else {
|
||||
this.suggestionArea.classList.add('hidden');
|
||||
}
|
||||
// Neue Vorschläge hinzufügen
|
||||
suggestions.forEach((text, index) => {
|
||||
const pill = document.createElement('button');
|
||||
pill.className = 'suggestion-pill text-sm px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-all duration-200';
|
||||
pill.style.animationDelay = `${index * 0.1}s`;
|
||||
pill.textContent = text;
|
||||
this.suggestionArea.appendChild(pill);
|
||||
});
|
||||
|
||||
// Vorschlagsbereich anzeigen
|
||||
this.suggestionArea.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -512,26 +445,33 @@ class ChatGPTAssistant {
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt einen Ladeindikator im Chat an
|
||||
* Zeigt eine Ladeanimation an
|
||||
*/
|
||||
showLoadingIndicator() {
|
||||
if (!this.chatHistory) return;
|
||||
|
||||
// Entferne vorhandenen Ladeindikator (falls vorhanden)
|
||||
this.removeLoadingIndicator();
|
||||
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||
if (document.getElementById('assistant-loading-indicator')) return;
|
||||
|
||||
const loadingEl = document.createElement('div');
|
||||
loadingEl.id = 'assistant-loading';
|
||||
loadingEl.className = 'flex justify-start';
|
||||
loadingEl.id = 'assistant-loading-indicator';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
||||
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
||||
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||
|
||||
const typingIndicator = document.createElement('div');
|
||||
typingIndicator.className = 'typing-indicator';
|
||||
typingIndicator.innerHTML = `
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
`;
|
||||
|
||||
bubble.appendChild(typingIndicator);
|
||||
loadingEl.appendChild(bubble);
|
||||
this.chatHistory.appendChild(loadingEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.appendChild(loadingEl);
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
}
|
||||
|
||||
@@ -539,7 +479,7 @@ class ChatGPTAssistant {
|
||||
* Entfernt den Ladeindikator aus dem Chat
|
||||
*/
|
||||
removeLoadingIndicator() {
|
||||
const loadingIndicator = document.getElementById('assistant-loading');
|
||||
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.remove();
|
||||
}
|
||||
|
||||
@@ -198,6 +198,32 @@
|
||||
body:not(.dark) .card:hover {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--light-primary-hover);
|
||||
}
|
||||
|
||||
/* Light Mode Buttons */
|
||||
body:not(.dark) .btn,
|
||||
body:not(.dark) button:not(.toggle) {
|
||||
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(91, 33, 182, 0.25);
|
||||
border-radius: 8px;
|
||||
padding: 0.625rem 1.25rem;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn:hover,
|
||||
body:not(.dark) button:not(.toggle):hover {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||
@@ -307,7 +333,7 @@
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
||||
: 'bg-gradient-to-r from-purple-600/30 to-indigo-500/30 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||
: 'bg-gradient-to-r from-purple-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -467,7 +493,7 @@
|
||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
||||
: 'bg-gradient-to-r from-purple-500/10 to-blue-400/10 text-gray-900 hover:from-purple-500/20 hover:to-blue-400/20'">
|
||||
: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
|
||||
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -638,36 +664,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);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
316
templates/create_mindmap.html
Normal file
316
templates/create_mindmap.html
Normal file
@@ -0,0 +1,316 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Mindmap erstellen{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Spezifische Stile für die Mindmap-Erstellungsseite */
|
||||
.form-container {
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body.dark .form-container {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .form-container {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .form-header {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body.dark .form-input,
|
||||
body.dark .form-textarea {
|
||||
background-color: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
body:not(.dark) .form-input,
|
||||
body:not(.dark) .form-textarea {
|
||||
background-color: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
body.dark .form-input:focus,
|
||||
body.dark .form-textarea:focus {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body:not(.dark) .form-input:focus,
|
||||
body:not(.dark) .form-textarea:focus {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-switch input[type="checkbox"] {
|
||||
height: 0;
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.form-switch label {
|
||||
cursor: pointer;
|
||||
width: 50px;
|
||||
height: 25px;
|
||||
background: rgba(100, 116, 139, 0.3);
|
||||
display: block;
|
||||
border-radius: 25px;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-switch label:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
background: #fff;
|
||||
border-radius: 19px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.form-switch input:checked + label {
|
||||
background: #7c3aed;
|
||||
}
|
||||
|
||||
.form-switch input:checked + label:after {
|
||||
left: calc(100% - 3px);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background-color: #7c3aed;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background-color: #6d28d9;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: transparent;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
body.dark .btn-cancel {
|
||||
color: #e2e8f0;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-cancel {
|
||||
color: #475569;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
body.dark .btn-cancel:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .btn-cancel:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Animation für den Seiteneintritt */
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
animation: slideInUp 0.5s ease forwards;
|
||||
}
|
||||
|
||||
/* Animation für Hover-Effekte */
|
||||
.input-animation {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.input-animation:focus {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8 animate-fadeIn">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<!-- Titel mit Animation -->
|
||||
<div class="text-center mb-8 animate-pulse">
|
||||
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
|
||||
Neue Mindmap erstellen
|
||||
</h1>
|
||||
<p class="opacity-80">Erstelle deine eigene Wissenslandkarte und organisiere deine Gedanken</p>
|
||||
</div>
|
||||
|
||||
<div class="form-container">
|
||||
<div class="form-header">
|
||||
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-body">
|
||||
<form action="{{ url_for('create_mindmap') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label">Name der Mindmap</label>
|
||||
<input type="text" id="name" name="name" class="form-input input-animation" required placeholder="z.B. Meine Philosophie-Mindmap">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">Beschreibung</label>
|
||||
<textarea id="description" name="description" class="form-textarea input-animation" placeholder="Worum geht es in dieser Mindmap?"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-switch">
|
||||
<input type="checkbox" id="is_private" name="is_private" checked>
|
||||
<label for="is_private"></label>
|
||||
<span>Private Mindmap (nur für dich sichtbar)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-6">
|
||||
<a href="{{ url_for('profile') }}" class="btn-cancel">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Zurück
|
||||
</a>
|
||||
<button type="submit" class="btn-submit">
|
||||
<i class="fas fa-save"></i>
|
||||
Mindmap erstellen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tipps-Sektion -->
|
||||
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
|
||||
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||
<h3 class="text-xl font-semibold mb-3"
|
||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Erstellen einer Mindmap
|
||||
</h3>
|
||||
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
<ul class="list-disc pl-5 space-y-2">
|
||||
<li>Wähle einen prägnanten, aber aussagekräftigen Namen für deine Mindmap</li>
|
||||
<li>Beginne mit einem zentralen Konzept und arbeite dich nach außen vor</li>
|
||||
<li>Verwende verschiedene Farben für unterschiedliche Kategorien oder Themenbereiche</li>
|
||||
<li>Füge Notizen zu Knoten hinzu, um komplexere Ideen zu erklären</li>
|
||||
<li>Verknüpfe verwandte Konzepte, um Beziehungen zu visualisieren</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Einfache Animationen für die Eingabefelder
|
||||
const inputs = document.querySelectorAll('.input-animation');
|
||||
|
||||
inputs.forEach(input => {
|
||||
// Subtile Skalierung bei Fokus
|
||||
input.addEventListener('focus', function() {
|
||||
this.style.transform = 'scale(1.01)';
|
||||
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
this.style.transform = 'scale(1)';
|
||||
this.style.boxShadow = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Formular-Absenden-Animation
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
const submitBtn = this.querySelector('.btn-submit');
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird erstellt...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -381,103 +381,143 @@
|
||||
}
|
||||
|
||||
/* Light Mode Anpassungen */
|
||||
html.light .profile-container,
|
||||
html.light .profile-tabs,
|
||||
html.light .activity-card,
|
||||
html.light .settings-card {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
body:not(.dark) .profile-container,
|
||||
body:not(.dark) .profile-tabs,
|
||||
body:not(.dark) .activity-card,
|
||||
body:not(.dark) .settings-card {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
html.light .avatar-container {
|
||||
body:not(.dark) .glass-card {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
body:not(.dark) .avatar-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 3px solid rgba(126, 63, 242, 0.3);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), 0 0 15px rgba(126, 63, 242, 0.15);
|
||||
}
|
||||
|
||||
html.light .user-info h1 {
|
||||
body:not(.dark) .user-info h1 {
|
||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html.light .user-bio,
|
||||
html.light .activity-content {
|
||||
body:not(.dark) .user-bio,
|
||||
body:not(.dark) .activity-content {
|
||||
color: #1a202c;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html.light .user-meta span {
|
||||
body:not(.dark) .user-meta span {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
html.light .stat-item,
|
||||
html.light .settings-input {
|
||||
body:not(.dark) .stat-item,
|
||||
body:not(.dark) .settings-input {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
html.light .stat-value {
|
||||
body:not(.dark) .stat-value {
|
||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||
}
|
||||
|
||||
html.light .stat-label {
|
||||
body:not(.dark) .stat-label {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
html.light .profile-tab {
|
||||
body:not(.dark) .profile-tab {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
html.light .profile-tab:hover {
|
||||
body:not(.dark) .profile-tab:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
html.light .profile-tab.active {
|
||||
body:not(.dark) .profile-tab.active {
|
||||
color: #7e3ff2;
|
||||
border-bottom: 3px solid #7e3ff2;
|
||||
background: rgba(126, 63, 242, 0.08);
|
||||
}
|
||||
|
||||
html.light .activity-title,
|
||||
html.light .settings-card-header,
|
||||
html.light .settings-label {
|
||||
body:not(.dark) .activity-title,
|
||||
body:not(.dark) .settings-card-header,
|
||||
body:not(.dark) .settings-label {
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
html.light .activity-date {
|
||||
body:not(.dark) .activity-date {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
html.light .activity-footer {
|
||||
body:not(.dark) .activity-footer {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
html.light .reaction-button {
|
||||
body:not(.dark) .reaction-button {
|
||||
color: #4a5568;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
html.light .reaction-button:hover {
|
||||
body:not(.dark) .reaction-button:hover {
|
||||
background: rgba(126, 63, 242, 0.1);
|
||||
color: #7e3ff2;
|
||||
}
|
||||
|
||||
html.light .reaction-button.active {
|
||||
body:not(.dark) .reaction-button.active {
|
||||
background: rgba(126, 63, 242, 0.15);
|
||||
color: #7e3ff2;
|
||||
}
|
||||
|
||||
html.light .action-button {
|
||||
body:not(.dark) .action-button {
|
||||
background: rgba(126, 63, 242, 0.1);
|
||||
color: #7e3ff2;
|
||||
border: 1px solid rgba(126, 63, 242, 0.2);
|
||||
}
|
||||
|
||||
html.light .action-button:hover {
|
||||
body:not(.dark) .action-button:hover {
|
||||
background: rgba(126, 63, 242, 0.2);
|
||||
}
|
||||
|
||||
/* Verbesserte Styles für Card-Items im Light Mode */
|
||||
body:not(.dark) .thought-item,
|
||||
body:not(.dark) .mindmap-item,
|
||||
body:not(.dark) .collection-item {
|
||||
background: white !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .thought-item:hover,
|
||||
body:not(.dark) .mindmap-item:hover,
|
||||
body:not(.dark) .collection-item:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-3px);
|
||||
border: 1px solid rgba(126, 63, 242, 0.2) !important;
|
||||
}
|
||||
|
||||
body:not(.dark) .edit-profile-btn {
|
||||
background: #7e3ff2;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
body:not(.dark) .edit-profile-btn:hover {
|
||||
background: #6d28d9;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.25);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -613,21 +653,42 @@
|
||||
<div class="tab-content hidden" id="thoughts-tab">
|
||||
<div id="thoughts-container">
|
||||
{% if thoughts %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for thought in thoughts %}
|
||||
<div class="thought-item">
|
||||
<h3>{{ thought.title }}</h3>
|
||||
<p>{{ thought.content }}</p>
|
||||
<div class="thought-meta">
|
||||
<span>{{ thought.date }}</span>
|
||||
<span>{{ thought.category }}</span>
|
||||
<div class="thought-item bg-opacity-70 rounded-xl overflow-hidden border transition-all duration-300 hover:transform hover:scale-105 hover:shadow-lg"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/80 border-gray-700/60' : 'bg-white/90 border-gray-200/60'">
|
||||
<div class="p-5" style="border-left: 4px solid {{ thought.color_code|default('#B39DDB') }}">
|
||||
<h3 class="text-xl font-bold mb-2"
|
||||
x-bind:class="darkMode ? 'text-purple-300' : 'text-purple-700'">{{ thought.title }}</h3>
|
||||
<p class="mb-4 text-sm"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
{{ thought.abstract or thought.content[:150] ~ '...' }}
|
||||
</p>
|
||||
<div class="flex justify-between items-center text-xs"
|
||||
x-bind:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
|
||||
<span>{{ thought.created_at.strftime('%d.%m.%Y') }}</span>
|
||||
<span>{{ thought.branch }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 border-t flex justify-between items-center"
|
||||
x-bind:class="darkMode ? 'bg-gray-900/80 border-gray-700/60' : 'bg-gray-50/80 border-gray-200/60'">
|
||||
<a href="{{ url_for('get_thought', thought_id=thought.id) }}" class="transition-colors"
|
||||
x-bind:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||
<i class="fas fa-eye mr-1"></i> Ansehen
|
||||
</a>
|
||||
<a href="{{ url_for('update_thought', thought_id=thought.id) }}" class="transition-colors"
|
||||
x-bind:class="darkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-500'">
|
||||
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
||||
<a href="{{ url_for('get_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a>
|
||||
<a href="{{ url_for('add_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">Ersten Gedanken erstellen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -639,20 +700,27 @@
|
||||
{% if user_mindmaps %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for mindmap in user_mindmaps %}
|
||||
<div class="mindmap-item bg-opacity-70 bg-gray-800 rounded-xl overflow-hidden border border-gray-700 transition-all duration-300 hover:transform hover:scale-105 hover:shadow-lg">
|
||||
<div class="mindmap-item bg-opacity-70 rounded-xl overflow-hidden border transition-all duration-300 hover:transform hover:scale-105 hover:shadow-lg"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/80 border-gray-700/60' : 'bg-white/90 border-gray-200/60'">
|
||||
<div class="p-5">
|
||||
<h3 class="text-xl font-bold text-purple-400 mb-2">{{ mindmap.name }}</h3>
|
||||
<p class="text-gray-300 mb-4 text-sm">{{ mindmap.description }}</p>
|
||||
<div class="flex justify-between items-center text-xs text-gray-400">
|
||||
<h3 class="text-xl font-bold mb-2"
|
||||
x-bind:class="darkMode ? 'text-purple-400' : 'text-purple-700'">{{ mindmap.name }}</h3>
|
||||
<p class="mb-4 text-sm"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">{{ mindmap.description }}</p>
|
||||
<div class="flex justify-between items-center text-xs"
|
||||
x-bind:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
|
||||
<span>Erstellt: {{ mindmap.created_at.strftime('%d.%m.%Y') }}</span>
|
||||
<span>Zuletzt bearbeitet: {{ mindmap.last_modified.strftime('%d.%m.%Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-900 p-3 border-t border-gray-700 flex justify-between">
|
||||
<a href="{{ url_for('mindmap') }}?id={{ mindmap.id }}" class="text-purple-400 hover:text-purple-300 transition-colors">
|
||||
<div class="p-3 border-t flex justify-between"
|
||||
x-bind:class="darkMode ? 'bg-gray-900/80 border-gray-700/60' : 'bg-gray-50/80 border-gray-200/60'">
|
||||
<a href="{{ url_for('mindmap') }}?id={{ mindmap.id }}" class="transition-colors"
|
||||
x-bind:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||
<i class="fas fa-eye mr-1"></i> Anzeigen
|
||||
</a>
|
||||
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="text-blue-400 hover:text-blue-300 transition-colors">
|
||||
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="transition-colors"
|
||||
x-bind:class="darkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-500'">
|
||||
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||
</a>
|
||||
</div>
|
||||
@@ -688,7 +756,7 @@
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-folder-open text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Sammlungen erstellt</p>
|
||||
<a href="{{ url_for('create_collection') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Erste Sammlung erstellen</a>
|
||||
<a href="{{ url_for('profile') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">Zurück zum Profil</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -722,7 +790,7 @@
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-project-diagram text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Verbindungen erstellt</p>
|
||||
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Verbindungen in der Mindmap erstellen</a>
|
||||
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">Verbindungen in der Mindmap erstellen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user