Add user authentication, thoughts, and comments functionality; enhance mindmap visualization and UI
This commit is contained in:
@@ -1,14 +1,692 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Wissenschaftliche Mindmap</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Wissenschaftliche Mindmap</h1>
|
||||
<div id="mindmap"></div>
|
||||
<script src="{{ url_for('static', filename='mindmap.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Mindmap | Wissenschaftliche Mindmap{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.node {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.node circle {
|
||||
fill: rgba(255, 255, 255, 0.2);
|
||||
stroke: white;
|
||||
stroke-width: 1.5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.node text {
|
||||
font-size: 12px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.node--selected circle {
|
||||
fill: rgba(139, 92, 246, 0.6);
|
||||
r: 25;
|
||||
stroke: rgba(255, 255, 255, 0.8);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.link {
|
||||
fill: none;
|
||||
stroke: rgba(255, 255, 255, 0.3);
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.thoughts-panel {
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.thoughts-panel.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.thoughts-container {
|
||||
max-height: calc(100vh - 250px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for the thoughts panel */
|
||||
.thoughts-container::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.thoughts-container::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.thoughts-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.thoughts-container::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
/* Node color coding by category */
|
||||
.node-science circle {
|
||||
fill: rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
.node-humanities circle {
|
||||
fill: rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.node-technology circle {
|
||||
fill: rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.node-arts circle {
|
||||
fill: rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
|
||||
/* Add a glow effect on hover */
|
||||
.node:hover circle {
|
||||
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col lg:flex-row h-[calc(100vh-100px)]">
|
||||
<!-- Main mindmap visualization area -->
|
||||
<div class="flex-grow relative" id="mindmap-container">
|
||||
<div class="absolute top-4 left-4 z-10">
|
||||
<h1 class="text-2xl font-bold text-white glass px-4 py-2 inline-block">Wissenschaftliche Mindmap</h1>
|
||||
</div>
|
||||
|
||||
<!-- Zoom controls -->
|
||||
<div class="absolute bottom-4 left-4 glass p-2 flex space-x-2 z-10">
|
||||
<button id="zoom-in" class="w-8 h-8 flex items-center justify-center text-white hover:bg-white/20 rounded-full transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="zoom-out" class="w-8 h-8 flex items-center justify-center text-white hover:bg-white/20 rounded-full transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="reset-view" class="w-8 h-8 flex items-center justify-center text-white hover:bg-white/20 rounded-full transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="absolute top-4 right-4 glass p-3 z-10">
|
||||
<h3 class="text-sm font-semibold text-white mb-2">Kategorien</h3>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div class="flex items-center">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-indigo-600/60 mr-2"></span>
|
||||
<span class="text-white/90">Naturwissenschaften</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-green-500/60 mr-2"></span>
|
||||
<span class="text-white/90">Geisteswissenschaften</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-purple-500/60 mr-2"></span>
|
||||
<span class="text-white/90">Technologie</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-pink-500/60 mr-2"></span>
|
||||
<span class="text-white/90">Künste</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg id="mindmap" class="w-full h-full"></svg>
|
||||
<div id="tooltip" class="tooltip"></div>
|
||||
</div>
|
||||
|
||||
<!-- Thoughts panel (hidden by default) -->
|
||||
<div id="thoughts-panel" class="thoughts-panel fixed lg:relative right-0 top-0 lg:top-auto h-full lg:h-auto w-full sm:w-96 dark-glass z-20">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold text-white" id="selected-node-title">Keine Auswahl</h2>
|
||||
<button id="close-panel" class="text-white/70 hover:text-white lg:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="glass p-4 mb-6">
|
||||
<h3 class="text-sm font-medium text-white/80 mb-2">Teile deinen Gedanken</h3>
|
||||
<textarea id="thought-input" rows="3" placeholder="Was denkst du zu diesem Thema?"
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all resize-none"></textarea>
|
||||
<button id="submit-thought"
|
||||
class="mt-2 w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-medium px-4 py-2 rounded-lg transition-all">
|
||||
Gedanken teilen
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="glass p-4 mb-6 text-center">
|
||||
<p class="text-white/80 mb-2">Melde dich an, um deine Gedanken zu teilen</p>
|
||||
<a href="{{ url_for('login') }}" class="text-indigo-300 hover:text-white font-medium">Anmelden</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="text-lg font-medium text-white mb-3">Community Gedanken</h3>
|
||||
<div id="thoughts-container" class="thoughts-container space-y-4">
|
||||
<div class="text-center py-8 text-white/50">
|
||||
<p>Wähle einen Knoten aus, um Gedanken zu sehen</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comment Modal -->
|
||||
<div id="commentModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 hidden">
|
||||
<div class="dark-glass p-6 w-full max-w-lg">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-bold text-white" id="comment-modal-title">Kommentare</h3>
|
||||
<button onclick="closeCommentModal()" class="text-white/70 hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="glass p-4 mb-4">
|
||||
<p id="comment-thought-content" class="text-white mb-2"></p>
|
||||
<div class="flex justify-between items-center text-xs text-white/60">
|
||||
<span id="comment-thought-author"></span>
|
||||
<span id="comment-thought-time"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 max-h-60 overflow-y-auto" id="comments-list">
|
||||
<!-- Comments will be loaded here -->
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="mt-4">
|
||||
<textarea id="comment-input" rows="2" placeholder="Füge einen Kommentar hinzu..."
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all resize-none"></textarea>
|
||||
<button id="submit-comment"
|
||||
class="mt-2 w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-medium px-4 py-2 rounded-lg transition-all">
|
||||
Kommentar hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="glass p-4 text-center">
|
||||
<p class="text-white/80 mb-2">Melde dich an, um Kommentare zu hinterlassen</p>
|
||||
<a href="{{ url_for('login') }}" class="text-indigo-300 hover:text-white font-medium">Anmelden</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script>
|
||||
// Global variables
|
||||
let selectedNode = null;
|
||||
let currentThoughtId = null;
|
||||
const width = document.getElementById('mindmap-container').clientWidth;
|
||||
const height = document.getElementById('mindmap-container').clientHeight;
|
||||
const nodeCategories = {
|
||||
'Naturwissenschaften': 'node-science',
|
||||
'Geisteswissenschaften': 'node-humanities',
|
||||
'Technologie': 'node-technology',
|
||||
'Künste': 'node-arts'
|
||||
};
|
||||
|
||||
// Initialize D3 visualization
|
||||
const svg = d3.select('#mindmap')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
const g = svg.append('g');
|
||||
|
||||
// Zoom behavior
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.3, 3])
|
||||
.on('zoom', (event) => {
|
||||
g.attr('transform', event.transform);
|
||||
});
|
||||
|
||||
svg.call(zoom);
|
||||
|
||||
// Reset to initial view
|
||||
function resetView() {
|
||||
svg.transition().duration(750).call(
|
||||
zoom.transform,
|
||||
d3.zoomIdentity.translate(width / 2, height / 2).scale(0.8)
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize tooltip
|
||||
const tooltip = d3.select('#tooltip');
|
||||
|
||||
// Load mindmap data
|
||||
function loadMindmap() {
|
||||
fetch('/api/mindmap')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
renderMindmap(data[0]); // Assuming first item is the root node
|
||||
resetView();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading mindmap:', error);
|
||||
alert('Fehler beim Laden der Mindmap. Bitte die Seite neu laden.');
|
||||
});
|
||||
}
|
||||
|
||||
// Get node category
|
||||
function getNodeCategory(nodeName, rootCategories) {
|
||||
// Check if the node is one of the root categories
|
||||
if (nodeCategories[nodeName]) {
|
||||
return nodeCategories[nodeName];
|
||||
}
|
||||
|
||||
// Check parent categories for sub-nodes
|
||||
for (const category in rootCategories) {
|
||||
if (rootCategories[category].includes(nodeName)) {
|
||||
return nodeCategories[category];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Process data to track categories
|
||||
function processData(node, rootCategories = {}) {
|
||||
// Initialize categories for root node
|
||||
if (node.name in nodeCategories) {
|
||||
rootCategories[node.name] = [];
|
||||
}
|
||||
|
||||
// Record all children of a category
|
||||
if (node.children) {
|
||||
node.children.forEach(child => {
|
||||
// Add to parent category
|
||||
for (const category in rootCategories) {
|
||||
if (node.name === category || rootCategories[category].includes(node.name)) {
|
||||
rootCategories[category].push(child.name);
|
||||
}
|
||||
}
|
||||
// Process recursively
|
||||
processData(child, rootCategories);
|
||||
});
|
||||
}
|
||||
|
||||
return rootCategories;
|
||||
}
|
||||
|
||||
// Render the mindmap visualization
|
||||
function renderMindmap(data) {
|
||||
// Clear previous content
|
||||
g.selectAll('*').remove();
|
||||
|
||||
// Process data to track categories
|
||||
const rootCategories = processData(data);
|
||||
|
||||
// Create hierarchical layout
|
||||
const root = d3.hierarchy(data);
|
||||
|
||||
// Create tree layout
|
||||
const treeLayout = d3.tree()
|
||||
.size([height - 100, width - 200])
|
||||
.nodeSize([80, 200]);
|
||||
|
||||
treeLayout(root);
|
||||
|
||||
// Create links
|
||||
const links = g.selectAll('.link')
|
||||
.data(root.links())
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('class', 'link')
|
||||
.attr('d', d => {
|
||||
return `M${d.source.y},${d.source.x}
|
||||
C${(d.source.y + d.target.y) / 2},${d.source.x}
|
||||
${(d.source.y + d.target.y) / 2},${d.target.x}
|
||||
${d.target.y},${d.target.x}`;
|
||||
});
|
||||
|
||||
// Create nodes
|
||||
const nodes = g.selectAll('.node')
|
||||
.data(root.descendants())
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', d => {
|
||||
const categoryClass = getNodeCategory(d.data.name, rootCategories);
|
||||
return `node ${categoryClass} ${d.data.id === selectedNode ? 'node--selected' : ''}`;
|
||||
})
|
||||
.attr('transform', d => `translate(${d.y},${d.x})`)
|
||||
.on('click', (event, d) => selectNode(d.data.id, d.data.name))
|
||||
.on('mouseover', function(event, d) {
|
||||
tooltip.transition()
|
||||
.duration(200)
|
||||
.style('opacity', 0.9);
|
||||
tooltip.html(d.data.name)
|
||||
.style('left', (event.pageX + 10) + 'px')
|
||||
.style('top', (event.pageY - 28) + 'px');
|
||||
})
|
||||
.on('mouseout', function(d) {
|
||||
tooltip.transition()
|
||||
.duration(500)
|
||||
.style('opacity', 0);
|
||||
});
|
||||
|
||||
// Add circles to nodes
|
||||
nodes.append('circle')
|
||||
.attr('r', 20)
|
||||
.attr('class', d => (d.depth === 0) ? 'root-node' : ''); // Special class for root node
|
||||
|
||||
// Add text labels
|
||||
nodes.append('text')
|
||||
.attr('dy', 30)
|
||||
.attr('text-anchor', 'middle')
|
||||
.text(d => d.data.name)
|
||||
.each(function(d) {
|
||||
// Wrap text for long labels
|
||||
const text = d3.select(this);
|
||||
const words = d.data.name.split(/\s+/);
|
||||
|
||||
if (words.length > 1) {
|
||||
text.text('');
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const tspan = text.append('tspan')
|
||||
.attr('x', 0)
|
||||
.attr('dy', i === 0 ? 30 : 15)
|
||||
.attr('text-anchor', 'middle')
|
||||
.text(words[i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add node count indicator (how many thoughts are associated)
|
||||
nodes.each(function(d) {
|
||||
const node = d3.select(this);
|
||||
|
||||
// Get thought count for this node (an API call would be needed)
|
||||
fetch(`/api/thoughts/${d.data.id}`)
|
||||
.then(response => response.json())
|
||||
.then(thoughts => {
|
||||
if (thoughts.length > 0) {
|
||||
// Add a small indicator
|
||||
node.append('circle')
|
||||
.attr('r', 8)
|
||||
.attr('cx', 20)
|
||||
.attr('cy', -20)
|
||||
.attr('fill', 'rgba(139, 92, 246, 0.9)');
|
||||
|
||||
node.append('text')
|
||||
.attr('x', 20)
|
||||
.attr('y', -16)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', 'white')
|
||||
.attr('font-size', '10px')
|
||||
.text(thoughts.length);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error(`Error loading thoughts for node ${d.data.id}:`, error));
|
||||
});
|
||||
}
|
||||
|
||||
// Handle node selection
|
||||
function selectNode(nodeId, nodeName) {
|
||||
selectedNode = nodeId;
|
||||
|
||||
// Update selected node in visualization
|
||||
g.selectAll('.node').classed('node--selected', d => d.data.id === nodeId);
|
||||
|
||||
// Update panel title
|
||||
document.getElementById('selected-node-title').textContent = nodeName;
|
||||
|
||||
// Load thoughts for this node
|
||||
loadThoughts(nodeId);
|
||||
|
||||
// Open the thoughts panel on mobile
|
||||
document.getElementById('thoughts-panel').classList.add('open');
|
||||
}
|
||||
|
||||
// Load thoughts for a node
|
||||
function loadThoughts(nodeId) {
|
||||
const thoughtsContainer = document.getElementById('thoughts-container');
|
||||
thoughtsContainer.innerHTML = '<div class="text-center py-4"><div class="inline-block animate-spin rounded-full h-6 w-6 border-t-2 border-white"></div></div>';
|
||||
|
||||
fetch(`/api/thoughts/${nodeId}`)
|
||||
.then(response => response.json())
|
||||
.then(thoughts => {
|
||||
thoughtsContainer.innerHTML = '';
|
||||
|
||||
if (thoughts.length === 0) {
|
||||
thoughtsContainer.innerHTML = '<div class="text-center py-4 text-white/50"><p>Noch keine Gedanken zu diesem Thema</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
thoughts.forEach(thought => {
|
||||
const thoughtEl = document.createElement('div');
|
||||
thoughtEl.className = 'glass p-4 hover:shadow-lg transition-all';
|
||||
thoughtEl.innerHTML = `
|
||||
<p class="text-white mb-2">${thought.content}</p>
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="text-white/70">Von ${thought.author}</span>
|
||||
<span class="text-white/50">${thought.timestamp}</span>
|
||||
</div>
|
||||
<div class="mt-2 text-right">
|
||||
<button class="text-indigo-300 hover:text-white text-sm" onclick="openCommentModal(${thought.id})">
|
||||
${thought.comments_count} Kommentar(e) anzeigen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
thoughtsContainer.appendChild(thoughtEl);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading thoughts:', error);
|
||||
thoughtsContainer.innerHTML = '<div class="text-center py-4 text-red-300"><p>Fehler beim Laden der Gedanken</p></div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Submit a new thought
|
||||
function submitThought() {
|
||||
if (!selectedNode) {
|
||||
alert('Bitte wähle zuerst einen Knoten aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
const thoughtInput = document.getElementById('thought-input');
|
||||
const content = thoughtInput.value.trim();
|
||||
|
||||
if (!content) {
|
||||
alert('Bitte gib einen Gedanken ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/thoughts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
node_id: selectedNode,
|
||||
content: content
|
||||
}),
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
thoughtInput.value = '';
|
||||
loadThoughts(selectedNode); // Reload thoughts
|
||||
|
||||
// Refresh node counts in visualization
|
||||
loadMindmap();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error adding thought:', error);
|
||||
alert('Fehler beim Hinzufügen des Gedankens.');
|
||||
});
|
||||
}
|
||||
|
||||
// Open comment modal
|
||||
function openCommentModal(thoughtId) {
|
||||
currentThoughtId = thoughtId;
|
||||
|
||||
// Get thought details
|
||||
fetch(`/api/thought/${thoughtId}`)
|
||||
.then(response => response.json())
|
||||
.then(thought => {
|
||||
document.getElementById('comment-thought-content').textContent = thought.content;
|
||||
document.getElementById('comment-thought-author').textContent = `Von ${thought.author}`;
|
||||
document.getElementById('comment-thought-time').textContent = thought.timestamp;
|
||||
|
||||
// Get comments
|
||||
return fetch(`/api/comments/${thoughtId}`);
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(comments => {
|
||||
const commentsList = document.getElementById('comments-list');
|
||||
commentsList.innerHTML = '';
|
||||
|
||||
if (comments.length === 0) {
|
||||
commentsList.innerHTML = '<p class="text-center text-white/50 py-3">Keine Kommentare vorhanden</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
comments.forEach(comment => {
|
||||
const commentEl = document.createElement('div');
|
||||
commentEl.className = 'glass p-3 mb-2';
|
||||
commentEl.innerHTML = `
|
||||
<p class="text-white text-sm">${comment.content}</p>
|
||||
<div class="flex justify-between items-center mt-1 text-xs">
|
||||
<span class="text-white/70">${comment.author}</span>
|
||||
<span class="text-white/50">${comment.timestamp}</span>
|
||||
</div>
|
||||
`;
|
||||
commentsList.appendChild(commentEl);
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error loading comments:', error));
|
||||
|
||||
document.getElementById('commentModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Close comment modal
|
||||
function closeCommentModal() {
|
||||
document.getElementById('commentModal').classList.add('hidden');
|
||||
currentThoughtId = null;
|
||||
}
|
||||
|
||||
// Submit a new comment
|
||||
function submitComment() {
|
||||
if (!currentThoughtId) return;
|
||||
|
||||
const commentInput = document.getElementById('comment-input');
|
||||
const content = commentInput.value.trim();
|
||||
|
||||
if (!content) {
|
||||
alert('Bitte gib einen Kommentar ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/comments', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
thought_id: currentThoughtId,
|
||||
content: content
|
||||
}),
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
commentInput.value = '';
|
||||
|
||||
// Refresh comments
|
||||
fetch(`/api/comments/${currentThoughtId}`)
|
||||
.then(response => response.json())
|
||||
.then(comments => {
|
||||
const commentsList = document.getElementById('comments-list');
|
||||
commentsList.innerHTML = '';
|
||||
|
||||
comments.forEach(comment => {
|
||||
const commentEl = document.createElement('div');
|
||||
commentEl.className = 'glass p-3 mb-2';
|
||||
commentEl.innerHTML = `
|
||||
<p class="text-white text-sm">${comment.content}</p>
|
||||
<div class="flex justify-between items-center mt-1 text-xs">
|
||||
<span class="text-white/70">${comment.author}</span>
|
||||
<span class="text-white/50">${comment.timestamp}</span>
|
||||
</div>
|
||||
`;
|
||||
commentsList.appendChild(commentEl);
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error refreshing comments:', error));
|
||||
|
||||
// Refresh thoughts since comment count changed
|
||||
if (selectedNode) {
|
||||
loadThoughts(selectedNode);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error adding comment:', error);
|
||||
alert('Fehler beim Hinzufügen des Kommentars.');
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load mindmap on page load
|
||||
loadMindmap();
|
||||
|
||||
// Submit thought
|
||||
document.getElementById('submit-thought')?.addEventListener('click', submitThought);
|
||||
|
||||
// Submit comment
|
||||
document.getElementById('submit-comment')?.addEventListener('click', submitComment);
|
||||
|
||||
// Close panel on mobile
|
||||
document.getElementById('close-panel')?.addEventListener('click', function() {
|
||||
document.getElementById('thoughts-panel').classList.remove('open');
|
||||
});
|
||||
|
||||
// Zoom controls
|
||||
document.getElementById('zoom-in')?.addEventListener('click', function() {
|
||||
svg.transition().duration(300).call(zoom.scaleBy, 1.3);
|
||||
});
|
||||
|
||||
document.getElementById('zoom-out')?.addEventListener('click', function() {
|
||||
svg.transition().duration(300).call(zoom.scaleBy, 0.7);
|
||||
});
|
||||
|
||||
document.getElementById('reset-view')?.addEventListener('click', resetView);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user