Files
website/templates/user_mindmap.html

550 lines
17 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ mindmap.name }} - Mindmap{% endblock %}
{% block extra_css %}
<style>
/* Container für die Mindmap mit verbesserten Glaseffekten */
#cy {
width: 100%;
height: calc(100vh - 14rem);
background-color: rgba(15, 23, 42, 0.3);
border-radius: 1rem;
overflow: hidden;
}
body:not(.dark) #cy {
background-color: rgba(255, 255, 255, 0.3);
}
/* Info-Panel */
.node-info-panel {
position: absolute;
top: 1rem;
right: 1rem;
width: 300px;
max-height: calc(100vh - 16rem);
overflow-y: auto;
border-radius: 1rem;
opacity: 0;
transform: translateX(20px);
pointer-events: none;
transition: all 0.3s ease;
}
.node-info-panel.visible {
opacity: 1;
transform: translateX(0);
pointer-events: all;
}
body.dark .node-info-panel {
background-color: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
body:not(.dark) .node-info-panel {
background-color: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
color: #1e293b;
}
/* Toolbar */
.mindmap-toolbar {
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
}
body.dark .mindmap-toolbar {
background-color: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) .mindmap-toolbar {
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.1);
}
.mindmap-action-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
cursor: pointer;
}
body.dark .mindmap-action-btn {
background-color: rgba(30, 41, 59, 0.7);
color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) .mindmap-action-btn {
background-color: rgba(255, 255, 255, 0.8);
color: #1e293b;
border: 1px solid rgba(0, 0, 0, 0.1);
}
body.dark .mindmap-action-btn:hover {
background-color: rgba(51, 65, 85, 0.8);
transform: translateY(-2px);
}
body:not(.dark) .mindmap-action-btn:hover {
background-color: rgba(243, 244, 246, 0.9);
transform: translateY(-2px);
}
/* Node-Link Styles */
.node-link {
display: inline-block;
padding: 0.35rem 0.75rem;
margin: 0.25rem;
border-radius: 1rem;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
color: white;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.node-link:hover {
transform: translateY(-2px);
filter: brightness(1.1);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
}
/* Notes */
.note-container {
position: absolute;
width: 250px;
cursor: move;
opacity: 0;
transform: scale(0.95);
transition: all 0.3s ease;
z-index: 10;
}
.note-container.visible {
opacity: 1;
transform: scale(1);
}
/* Note-Werkzeuge */
.note-tools {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
}
.note-tool {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.7rem;
opacity: 0.7;
}
.note-tool:hover {
opacity: 1;
transform: translateY(-1px);
}
/* Color Palette */
.color-palette {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
padding: 0.25rem;
}
.color-swatch {
width: 1rem;
height: 1rem;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
}
.color-swatch:hover {
transform: scale(1.2);
}
/* Notizen-Editor */
.note-content {
min-height: 100px;
padding: 0.75rem;
outline: none;
width: 100%;
font-size: 0.9rem;
resize: both;
overflow: auto;
}
/* Kontext-Menü */
#context-menu {
min-width: 180px;
border-radius: 0.5rem;
overflow: hidden;
z-index: 1000;
}
body.dark #context-menu {
background-color: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
body:not(.dark) #context-menu {
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
color: #1e293b;
}
.menu-item {
padding: 0.75rem 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
body.dark .menu-item:hover {
background-color: rgba(255, 255, 255, 0.1);
}
body:not(.dark) .menu-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Dialogfenster */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.dialog-content {
width: 90%;
max-width: 500px;
border-radius: 1rem;
padding: 1.5rem;
max-height: 80vh;
overflow-y: auto;
}
body.dark .dialog-content {
background-color: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) .dialog-content {
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.1);
color: #1e293b;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto max-w-7xl px-4 py-6">
<!-- Header -->
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<div>
<h1 class="text-3xl font-bold gradient-text">{{ mindmap.name }}</h1>
<p class="opacity-80 mt-1">{{ mindmap.description }}</p>
</div>
<div class="flex gap-3 flex-wrap">
{% if mindmap.user_id == current_user.id %}
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="mindmap-action-btn">
<i class="fas fa-edit"></i>
<span>Bearbeiten</span>
</a>
{% endif %}
<a href="{{ url_for('profile') }}" class="mindmap-action-btn">
<i class="fas fa-arrow-left"></i>
<span>Zurück</span>
</a>
</div>
</div>
<!-- Toolbar -->
<div class="mindmap-toolbar mb-4">
<button id="fit-btn" class="mindmap-action-btn">
<i class="fas fa-expand"></i>
<span>Alles anzeigen</span>
</button>
<button id="reset-btn" class="mindmap-action-btn">
<i class="fas fa-redo"></i>
<span>Layout zurücksetzen</span>
</button>
<button id="toggle-labels-btn" class="mindmap-action-btn">
<i class="fas fa-tags"></i>
<span>Labels ein/aus</span>
</button>
<button id="add-note-btn" class="mindmap-action-btn">
<i class="fas fa-sticky-note"></i>
<span>Notiz hinzufügen</span>
</button>
<button id="save-layout-btn" class="mindmap-action-btn">
<i class="fas fa-save"></i>
<span>Layout speichern</span>
</button>
</div>
<!-- Mindmap Container mit Positionsindikator -->
<div class="relative rounded-xl overflow-hidden border transition-all duration-300"
x-bind:class="darkMode ? 'border-gray-700/50' : 'border-gray-300/50'">
<div id="cy" data-mindmap-id="{{ mindmap.id }}"></div>
<!-- Informationsanzeige für ausgewählten Knoten -->
<div id="node-info-panel" class="node-info-panel p-4">
<h3 class="text-xl font-bold gradient-text mb-2">Knotendetails</h3>
<p id="node-description" class="mb-4 opacity-80"></p>
<h4 class="font-semibold mb-2 opacity-90">Verbundene Knoten</h4>
<div id="connected-nodes" class="flex flex-wrap mb-4"></div>
<div class="flex flex-wrap gap-2 mt-4">
<button id="add-thought-btn" class="mindmap-action-btn">
<i class="fas fa-lightbulb"></i>
<span>Gedanke hinzufügen</span>
</button>
<button id="view-thoughts-btn" class="mindmap-action-btn">
<i class="fas fa-list"></i>
<span>Gedanken anzeigen</span>
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', async function() {
const cyContainer = document.getElementById('cy');
if (!cyContainer) {
console.error("Mindmap container #cy not found!");
return;
}
const mindmapId = cyContainer.dataset.mindmapId;
const nodeInfoPanel = document.getElementById('node-info-panel');
const nodeDescription = document.getElementById('node-description');
const connectedNodesContainer = document.getElementById('connected-nodes');
const mindmapNameH1 = document.querySelector('h1.gradient-text');
const mindmapDescriptionP = document.querySelector('p.opacity-80.mt-1');
// Funktion zum Anzeigen von Benachrichtigungen (vereinfacht)
function showUINotification(message, type = 'success') {
const notificationArea = document.getElementById('notification-area-usr') || createUINotificationArea();
const notificationId = `notif-usr-${Date.now()}`;
const bgColor = type === 'success' ? 'bg-green-500' : (type === 'error' ? 'bg-red-500' : 'bg-blue-500');
const notificationElement = `
<div id="${notificationId}" class="p-3 mb-3 text-sm text-white rounded-lg ${bgColor} animate-fadeIn" role="alert">
<span>${message}</span>
</div>
`;
notificationArea.insertAdjacentHTML('beforeend', notificationElement);
setTimeout(() => {
const el = document.getElementById(notificationId);
if (el) {
el.classList.add('animate-fadeOut');
setTimeout(() => el.remove(), 500);
}
}, 3000);
}
function createUINotificationArea() {
const area = document.createElement('div');
area.id = 'notification-area-usr';
area.className = 'fixed top-20 right-5 z-[1001] w-auto max-w-xs'; // höhere z-index
document.body.appendChild(area);
const style = document.createElement('style');
style.textContent = `
.animate-fadeIn { animation: fadeIn 0.3s ease-out; }
.animate-fadeOut { animation: fadeOut 0.3s ease-in forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
@keyframes fadeOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(20px); } }
`;
document.head.appendChild(style);
return area;
}
async function fetchMindmapData(id) {
try {
const response = await fetch(`/api/mindmaps/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Fehler beim Laden der Mindmap-Daten:', error);
showUINotification('Fehler beim Laden der Mindmap-Daten.', 'error');
cyContainer.innerHTML = '<p class="text-center text-red-500 p-10">Konnte Mindmap nicht laden.</p>';
return null;
}
}
const mindmapData = await fetchMindmapData(mindmapId);
if (mindmapData) {
if(mindmapNameH1) mindmapNameH1.textContent = mindmapData.name;
if(mindmapDescriptionP) mindmapDescriptionP.textContent = mindmapData.description || "Keine Beschreibung vorhanden.";
// Cytoscape initialisieren
const cy = cytoscape({
container: cyContainer,
elements: mindmapData.elements || [], // Verwende 'elements' aus der API-Antwort
style: [
{
selector: 'node',
style: {
'background-color': 'data(color)',
'label': 'data(label)',
'color': 'data(fontColor)',
'text-valign': 'center',
'text-halign': 'center',
'font-size': 'data(fontSize)',
'width': ele => ele.data('isCenter') ? 60 : (ele.data('size') || 40),
'height': ele => ele.data('isCenter') ? 60 : (ele.data('size') || 40),
'border-width': 2,
'border-color': '#fff',
'shape': 'ellipse',
'text-outline-color': '#555',
'text-outline-width': 1,
}
},
{
selector: 'edge',
style: {
'width': ele => ele.data('strength') ? ele.data('strength') * 1.5 : 2,
'line-color': ele => ele.data('color') || '#9dbaea',
'target-arrow-color': ele => ele.data('color') || '#9dbaea',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier'
}
},
{
selector: 'node:selected',
style: {
'border-color': '#f59e42',
'border-width': 3,
}
}
],
layout: {
name: 'cose',
idealEdgeLength: 100,
nodeOverlap: 20,
refresh: 20,
fit: true,
padding: 30,
randomize: false,
componentSpacing: 100,
nodeRepulsion: 400000,
edgeElasticity: 100,
nestingFactor: 5,
gravity: 80,
numIter: 1000,
initialTemp: 200,
coolingFactor: 0.95,
minTemp: 1.0
}
});
window.cyInstance = cy; // Für globalen Zugriff falls nötig
cy.on('tap', 'node', function(evt){
const node = evt.target;
if (nodeDescription) nodeDescription.textContent = node.data('description') || 'Keine Beschreibung für diesen Knoten.';
if (connectedNodesContainer) {
connectedNodesContainer.innerHTML = '';
const connected = node.connectedEdges().otherNodes();
if (connected.length > 0) {
connected.forEach(cn => {
const link = document.createElement('span');
link.className = 'node-link';
link.textContent = cn.data('label');
link.style.backgroundColor = cn.data('color') || '#60a5fa';
link.addEventListener('click', () => {
cy.center(cn);
cn.select();
// Info Panel für den geklickten verbundenen Knoten aktualisieren
if (nodeDescription) nodeDescription.textContent = cn.data('description') || 'Keine Beschreibung für diesen Knoten.';
// Rekursiv verbundene Knoten des neu ausgewählten Knotens anzeigen (optional)
});
connectedNodesContainer.appendChild(link);
});
} else {
connectedNodesContainer.innerHTML = '<p class="opacity-70 text-sm">Keine direkten Verbindungen.</p>';
}
}
if (nodeInfoPanel) nodeInfoPanel.classList.add('visible');
});
cy.on('tap', function(evt){
if(evt.target === cy){ // Klick auf Hintergrund
if (nodeInfoPanel) nodeInfoPanel.classList.remove('visible');
}
});
// Toolbar-Buttons
document.getElementById('fit-btn')?.addEventListener('click', () => cy.fit(null, 50));
document.getElementById('reset-btn')?.addEventListener('click', () => cy.layout({name: 'cose', animate:true}).run());
let labelsVisible = true;
document.getElementById('toggle-labels-btn')?.addEventListener('click', () => {
labelsVisible = !labelsVisible;
cy.style().selector('node').style({'text-opacity': labelsVisible ? 1 : 0}).update();
});
// TODO: add-note-btn und save-layout-btn Funktionalität (benötigt /edit_mindmap Seite oder API Endpunkte)
document.getElementById('add-note-btn').addEventListener('click', function() {
showUINotification('Notizfunktion wird auf der Bearbeitungsseite implementiert.', 'info');
});
document.getElementById('save-layout-btn').addEventListener('click', function() {
showUINotification('Layout-Speicherung wird auf der Bearbeitungsseite implementiert.', 'info');
});
} else {
// Fallback, falls mindmapData null ist
if(mindmapNameH1) mindmapNameH1.textContent = "Mindmap nicht gefunden";
cyContainer.innerHTML = '<p class="text-center text-red-500 p-10">Die angeforderte Mindmap konnte nicht geladen werden.</p>';
}
}); // End of DOMContentLoaded
</script>
{% endblock %}