✨ feat: enhance UI and functionality for mindmap creation and profile pages
This commit is contained in:
160
app.py
160
app.py
@@ -628,16 +628,156 @@ def delete_mindmap(mindmap_id):
|
|||||||
# API-Endpunkte für Mindmap-Daten
|
# API-Endpunkte für Mindmap-Daten
|
||||||
@app.route('/api/mindmap/public')
|
@app.route('/api/mindmap/public')
|
||||||
def get_public_mindmap():
|
def get_public_mindmap():
|
||||||
"""Liefert die öffentliche Mindmap-Struktur."""
|
"""Liefert die Standard-Mindmap-Struktur basierend auf Kategorien."""
|
||||||
# Hole alle Kategorien der obersten Ebene
|
try:
|
||||||
root_categories = Category.query.filter_by(parent_id=None).all()
|
# Hole alle Hauptkategorien
|
||||||
|
categories = Category.query.filter_by(parent_id=None).all()
|
||||||
# Baue Baumstruktur auf
|
|
||||||
result = []
|
# Transformiere zu einer Baumstruktur
|
||||||
for category in root_categories:
|
category_tree = [build_category_tree(cat) for cat in categories]
|
||||||
result.append(build_category_tree(category))
|
|
||||||
|
return jsonify(category_tree)
|
||||||
return jsonify(result)
|
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.utcnow()
|
||||||
|
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):
|
def build_category_tree(category):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -68,18 +68,37 @@ body {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Mode */
|
/* Strikte Trennung: Dark Mode */
|
||||||
html.dark body {
|
html.dark body,
|
||||||
|
body.dark {
|
||||||
background-color: var(--bg-primary-dark);
|
background-color: var(--bg-primary-dark);
|
||||||
color: var(--text-primary-dark);
|
color: var(--text-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode */
|
/* Strikte Trennung: Light Mode */
|
||||||
|
html:not(.dark) body,
|
||||||
body:not(.dark) {
|
body:not(.dark) {
|
||||||
background-color: var(--light-bg);
|
background-color: var(--light-bg);
|
||||||
color: var(--light-text);
|
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 */
|
/* Typography */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -417,4 +417,606 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... existing code ...
|
||||||
}
|
}
|
||||||
@@ -1 +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 %}
|
||||||
@@ -627,7 +627,7 @@
|
|||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
||||||
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user