Refactor mindmap visualization and enhance user authentication UI: Implement API calls to load mindmap data dynamically, process hierarchical data into nodes and links, and improve error handling. Update login and registration templates for a modern design with enhanced validation and user experience. Remove obsolete network background images.
This commit is contained in:
@@ -282,21 +282,25 @@ class MindMapVisualization {
|
||||
// Zeige Lade-Animation
|
||||
this.showLoading();
|
||||
|
||||
// Demo-Logik: Verwende direkt die Standardknoten
|
||||
this.nodes = this.defaultNodes;
|
||||
this.links = this.defaultLinks;
|
||||
// API-Aufruf durchführen, um die Kategorien und ihre Knoten zu laden
|
||||
const response = await fetch('/api/mindmap/public');
|
||||
if (!response.ok) {
|
||||
throw new Error('API-Fehler: ' + response.statusText);
|
||||
}
|
||||
|
||||
// Simuliere einen API-Aufruf (in einer echten Anwendung würde hier ein Fetch stehen)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const data = await response.json();
|
||||
console.log('Geladene Mindmap-Daten:', data);
|
||||
|
||||
// Verarbeite die hierarchischen Daten in flache Knoten und Links
|
||||
const processed = this.processApiData(data);
|
||||
this.nodes = processed.nodes;
|
||||
this.links = processed.links;
|
||||
|
||||
// Visualisierung aktualisieren
|
||||
this.updateVisualization();
|
||||
|
||||
// Lade-Animation ausblenden
|
||||
this.hideLoading();
|
||||
|
||||
// Zufällige Knoten pulsieren lassen
|
||||
this.pulseRandomNodes();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
||||
|
||||
@@ -304,64 +308,140 @@ class MindMapVisualization {
|
||||
this.nodes = this.defaultNodes;
|
||||
this.links = this.defaultLinks;
|
||||
|
||||
// Fehler anzeigen
|
||||
this.showError('Mindmap-Daten konnten nicht geladen werden. Verwende Standarddaten.');
|
||||
|
||||
// Visualisierung auch im Fehlerfall aktualisieren
|
||||
this.updateVisualization();
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// Startet ein zufälliges Pulsen von Knoten für visuelle Aufmerksamkeit
|
||||
pulseRandomNodes() {
|
||||
// Zufälligen Knoten auswählen
|
||||
const randomNode = () => {
|
||||
const randomIndex = Math.floor(Math.random() * this.nodes.length);
|
||||
return this.nodes[randomIndex];
|
||||
// Verarbeitet die API-Daten in das benötigte Format
|
||||
processApiData(apiData) {
|
||||
// Erstelle einen Root-Knoten, der alle Kategorien verbindet
|
||||
const rootNode = {
|
||||
id: "root",
|
||||
name: "Wissen",
|
||||
description: "Zentrale Wissensbasis",
|
||||
thought_count: 0
|
||||
};
|
||||
|
||||
// Initiales Pulsen starten
|
||||
const initialPulse = () => {
|
||||
const node = randomNode();
|
||||
this.pulseNode(node);
|
||||
let nodes = [rootNode];
|
||||
let links = [];
|
||||
|
||||
// Für jede Kategorie Knoten und Verbindungen erstellen
|
||||
apiData.forEach(category => {
|
||||
// Kategorie als Knoten hinzufügen
|
||||
const categoryNode = {
|
||||
id: `category_${category.id}`,
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
color_code: category.color_code,
|
||||
icon: category.icon,
|
||||
thought_count: 0,
|
||||
type: 'category'
|
||||
};
|
||||
|
||||
// Nächstes Pulsen in 3-7 Sekunden
|
||||
setTimeout(() => {
|
||||
const nextNode = randomNode();
|
||||
this.pulseNode(nextNode);
|
||||
|
||||
// Regelmäßig wiederholen
|
||||
setInterval(() => {
|
||||
const pulseNode = randomNode();
|
||||
this.pulseNode(pulseNode);
|
||||
}, 5000 + Math.random() * 5000);
|
||||
}, 3000 + Math.random() * 4000);
|
||||
};
|
||||
nodes.push(categoryNode);
|
||||
|
||||
// Mit Root-Knoten verbinden
|
||||
links.push({
|
||||
source: "root",
|
||||
target: categoryNode.id
|
||||
});
|
||||
|
||||
// Alle Knoten aus dieser Kategorie hinzufügen
|
||||
if (category.nodes && category.nodes.length > 0) {
|
||||
category.nodes.forEach(node => {
|
||||
// Zähle die Gedanken für die Kategorie
|
||||
categoryNode.thought_count += node.thought_count || 0;
|
||||
|
||||
const mindmapNode = {
|
||||
id: `node_${node.id}`,
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
color_code: node.color_code || category.color_code,
|
||||
thought_count: node.thought_count || 0,
|
||||
type: 'node',
|
||||
categoryId: category.id
|
||||
};
|
||||
|
||||
nodes.push(mindmapNode);
|
||||
|
||||
// Mit Kategorie-Knoten verbinden
|
||||
links.push({
|
||||
source: categoryNode.id,
|
||||
target: mindmapNode.id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Rekursiv Unterkategorien verarbeiten
|
||||
if (category.children && category.children.length > 0) {
|
||||
this.processSubcategories(category.children, nodes, links, categoryNode.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Verzögertes Starten nach vollständigem Laden
|
||||
setTimeout(initialPulse, 1000);
|
||||
// Root-Knoten-Gedankenzähler aktualisieren
|
||||
rootNode.thought_count = nodes.reduce((sum, node) => sum + (node.thought_count || 0), 0);
|
||||
|
||||
return { nodes, links };
|
||||
}
|
||||
|
||||
// Lässt einen Knoten pulsieren für visuelle Hervorhebung
|
||||
pulseNode(node) {
|
||||
if (!this.nodeElements) return;
|
||||
|
||||
const nodeElement = this.nodeElements.filter(d => d.id === node.id);
|
||||
|
||||
if (nodeElement.size() > 0) {
|
||||
const circle = nodeElement.select('circle');
|
||||
// Verarbeitet Unterkategorien rekursiv
|
||||
processSubcategories(subcategories, nodes, links, parentId) {
|
||||
subcategories.forEach(category => {
|
||||
// Kategorie als Knoten hinzufügen
|
||||
const categoryNode = {
|
||||
id: `category_${category.id}`,
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
color_code: category.color_code,
|
||||
icon: category.icon,
|
||||
thought_count: 0,
|
||||
type: 'subcategory'
|
||||
};
|
||||
|
||||
// Speichern des ursprünglichen Radius
|
||||
const originalRadius = circle.attr('r');
|
||||
nodes.push(categoryNode);
|
||||
|
||||
// Animiertes Pulsieren
|
||||
circle.transition()
|
||||
.duration(600)
|
||||
.attr('r', originalRadius * 1.3)
|
||||
.attr('filter', 'url(#pulse-effect)')
|
||||
.transition()
|
||||
.duration(600)
|
||||
.attr('r', originalRadius)
|
||||
.attr('filter', 'url(#glass-effect)');
|
||||
}
|
||||
// Mit Eltern-Kategorie verbinden
|
||||
links.push({
|
||||
source: parentId,
|
||||
target: categoryNode.id
|
||||
});
|
||||
|
||||
// Alle Knoten aus dieser Kategorie hinzufügen
|
||||
if (category.nodes && category.nodes.length > 0) {
|
||||
category.nodes.forEach(node => {
|
||||
// Zähle die Gedanken für die Kategorie
|
||||
categoryNode.thought_count += node.thought_count || 0;
|
||||
|
||||
const mindmapNode = {
|
||||
id: `node_${node.id}`,
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
color_code: node.color_code || category.color_code,
|
||||
thought_count: node.thought_count || 0,
|
||||
type: 'node',
|
||||
categoryId: category.id
|
||||
};
|
||||
|
||||
nodes.push(mindmapNode);
|
||||
|
||||
// Mit Kategorie-Knoten verbinden
|
||||
links.push({
|
||||
source: categoryNode.id,
|
||||
target: mindmapNode.id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Rekursiv Unterkategorien verarbeiten
|
||||
if (category.children && category.children.length > 0) {
|
||||
this.processSubcategories(category.children, nodes, links, categoryNode.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Zeigt den Ladebildschirm an
|
||||
@@ -586,11 +666,20 @@ class MindMapVisualization {
|
||||
}
|
||||
}
|
||||
|
||||
// Farbe basierend auf Knotentyp erhalten
|
||||
// Bestimmt die Farbe eines Knotens basierend auf seinem Typ oder direkt angegebener Farbe
|
||||
getNodeColor(node) {
|
||||
// Verwende die ID als Typ, falls vorhanden
|
||||
const nodeType = node.id.toLowerCase();
|
||||
return this.colorPalette[nodeType] || this.colorPalette.default;
|
||||
// Direkt angegebene Farbe verwenden, wenn vorhanden
|
||||
if (node.color_code) {
|
||||
return node.color_code;
|
||||
}
|
||||
|
||||
// Kategorietyp-basierte Färbung
|
||||
if (node.type === 'category' || node.type === 'subcategory') {
|
||||
return this.colorPalette.root;
|
||||
}
|
||||
|
||||
// Fallback für verschiedene Knotentypen
|
||||
return this.colorPalette[node.id] || this.colorPalette.default;
|
||||
}
|
||||
|
||||
// Aktualisiert die Positionen in jedem Simulationsschritt
|
||||
@@ -1031,7 +1120,7 @@ class MindMapVisualization {
|
||||
|
||||
// Verzögerung für Animation
|
||||
setTimeout(() => {
|
||||
// API-Aufruf simulieren (später durch echten Aufruf ersetzen)
|
||||
// API-Aufruf für echte Daten aus der Datenbank
|
||||
this.fetchThoughtsForNode(node.id)
|
||||
.then(thoughts => {
|
||||
// Ladeanimation ausblenden
|
||||
@@ -1056,6 +1145,128 @@ class MindMapVisualization {
|
||||
}, 600); // Verzögerung für bessere UX
|
||||
}
|
||||
|
||||
// Holt Gedanken für einen Knoten aus der Datenbank
|
||||
async fetchThoughtsForNode(nodeId) {
|
||||
try {
|
||||
// Extrahiere die tatsächliche ID aus dem nodeId Format (z.B. "node_123" oder "category_456")
|
||||
const id = nodeId.toString().split('_')[1];
|
||||
if (!id) {
|
||||
console.warn('Ungültige Node-ID: ', nodeId);
|
||||
return [];
|
||||
}
|
||||
|
||||
// API-Aufruf an den entsprechenden Endpunkt
|
||||
const response = await fetch(`/api/nodes/${id}/thoughts`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API-Fehler: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const thoughts = await response.json();
|
||||
console.log('Geladene Gedanken für Knoten:', thoughts);
|
||||
return thoughts;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Gedanken für Knoten:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Rendert die Gedanken in der UI
|
||||
renderThoughts(thoughts, container) {
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
thoughts.forEach(thought => {
|
||||
const thoughtCard = document.createElement('div');
|
||||
thoughtCard.className = 'thought-card';
|
||||
thoughtCard.setAttribute('data-id', thought.id);
|
||||
|
||||
const cardColor = thought.color_code || this.colorPalette.default;
|
||||
|
||||
thoughtCard.innerHTML = `
|
||||
<div class="thought-card-header" style="border-left: 4px solid ${cardColor}">
|
||||
<h3 class="thought-title">${thought.title}</h3>
|
||||
<div class="thought-meta">
|
||||
<span class="thought-date">${new Date(thought.created_at).toLocaleDateString('de-DE')}</span>
|
||||
${thought.author ? `<span class="thought-author">von ${thought.author.username}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="thought-content">
|
||||
<p>${thought.abstract || thought.content.substring(0, 150) + '...'}</p>
|
||||
</div>
|
||||
<div class="thought-footer">
|
||||
<div class="thought-keywords">
|
||||
${thought.keywords ? thought.keywords.split(',').map(kw =>
|
||||
`<span class="keyword">${kw.trim()}</span>`).join('') : ''}
|
||||
</div>
|
||||
<a href="/thoughts/${thought.id}" class="thought-link">Mehr lesen →</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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') return;
|
||||
window.location.href = `/thoughts/${thought.id}`;
|
||||
});
|
||||
|
||||
container.appendChild(thoughtCard);
|
||||
});
|
||||
}
|
||||
|
||||
// Rendert eine Leermeldung, wenn keine Gedanken vorhanden sind
|
||||
renderEmptyThoughts(container, node) {
|
||||
if (!container) return;
|
||||
|
||||
const emptyState = document.createElement('div');
|
||||
emptyState.className = 'empty-thoughts-state';
|
||||
|
||||
emptyState.innerHTML = `
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-lightbulb"></i>
|
||||
</div>
|
||||
<h3>Keine Gedanken verknüpft</h3>
|
||||
<p>Zu "${node.name}" sind noch keine Gedanken verknüpft.</p>
|
||||
<div class="empty-actions">
|
||||
<a href="/add-thought?node=${node.id}" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle"></i> Gedanken hinzufügen
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(emptyState);
|
||||
}
|
||||
|
||||
// Rendert einen Fehlerzustand
|
||||
renderErrorState(container) {
|
||||
if (!container) return;
|
||||
|
||||
const errorState = document.createElement('div');
|
||||
errorState.className = 'error-thoughts-state';
|
||||
|
||||
errorState.innerHTML = `
|
||||
<div class="error-icon">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<h3>Fehler beim Laden</h3>
|
||||
<p>Die Gedanken konnten nicht geladen werden. Bitte versuche es später erneut.</p>
|
||||
<button class="btn btn-secondary retry-button">
|
||||
<i class="fas fa-redo"></i> Erneut versuchen
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Event-Listener für Retry-Button
|
||||
const retryButton = errorState.querySelector('.retry-button');
|
||||
if (retryButton && this.selectedNode) {
|
||||
retryButton.addEventListener('click', () => {
|
||||
this.loadThoughtsForNode(this.selectedNode);
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(errorState);
|
||||
}
|
||||
|
||||
// Zentriert einen Knoten in der Ansicht
|
||||
centerNodeInView(node) {
|
||||
// Sanfter Übergang zur Knotenzentrierüng
|
||||
|
||||
Reference in New Issue
Block a user