Add user authentication, thoughts, and comments functionality; enhance mindmap visualization and UI

This commit is contained in:
2025-04-20 18:48:14 +02:00
parent c8a9de2e43
commit 511639ed15
17 changed files with 1656 additions and 35 deletions

1
README.md Normal file
View File

@@ -0,0 +1 @@

View File

@@ -1 +1,6 @@
flask
flask
flask-login
flask-wtf
email-validator
python-dotenv
flask-sqlalchemy

Binary file not shown.

Binary file not shown.

View File

@@ -1,17 +1,264 @@
from flask import Flask, render_template
import os
from datetime import datetime
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import json
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///mindmap.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Route für die Startseite
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
# Database Models
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
thoughts = db.relationship('Thought', backref='author', lazy=True)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Thought(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
branch = db.Column(db.String(100), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
comments = db.relationship('Comment', backref='thought', lazy=True, cascade="all, delete-orphan")
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
author = db.relationship('User', backref='comments')
class MindMapNode(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
children = db.relationship('MindMapNode', backref=db.backref('parent', remote_side=[id]))
thoughts = db.relationship('Thought', secondary='node_thought_association', backref='nodes')
# Association table for many-to-many relationship between MindMapNode and Thought
node_thought_association = db.Table('node_thought_association',
db.Column('node_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
)
@login_manager.user_loader
def load_user(id):
return User.query.get(int(id))
# Routes for authentication
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
next_page = request.args.get('next')
return redirect(next_page or url_for('index'))
flash('Ungültiger Benutzername oder Passwort')
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
if User.query.filter_by(username=username).first():
flash('Benutzername existiert bereits')
return redirect(url_for('register'))
if User.query.filter_by(email=email).first():
flash('E-Mail ist bereits registriert')
return redirect(url_for('register'))
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
login_user(user)
return redirect(url_for('index'))
return render_template('register.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))
# Route for the homepage
@app.route('/')
def index():
return render_template('index.html')
# Route für die Mindmap-Seite
# Route for the mindmap page
@app.route('/mindmap')
def mindmap():
return render_template('mindmap.html')
# Route for user profile
@app.route('/profile')
@login_required
def profile():
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.timestamp.desc()).all()
return render_template('profile.html', thoughts=thoughts)
# API routes for mindmap and thoughts
@app.route('/api/mindmap')
def get_mindmap():
root_nodes = MindMapNode.query.filter_by(parent_id=None).all()
def build_tree(node):
return {
'id': node.id,
'name': node.name,
'children': [build_tree(child) for child in node.children]
}
result = [build_tree(node) for node in root_nodes]
return jsonify(result)
@app.route('/api/thoughts/<int:node_id>', methods=['GET'])
def get_thoughts(node_id):
node = MindMapNode.query.get_or_404(node_id)
thoughts = []
for thought in node.thoughts:
thoughts.append({
'id': thought.id,
'content': thought.content,
'author': thought.author.username,
'timestamp': thought.timestamp.strftime('%d.%m.%Y, %H:%M'),
'comments_count': len(thought.comments),
'branch': thought.branch
})
return jsonify(thoughts)
@app.route('/api/thought/<int:thought_id>', methods=['GET'])
def get_thought(thought_id):
thought = Thought.query.get_or_404(thought_id)
return jsonify({
'id': thought.id,
'content': thought.content,
'author': thought.author.username,
'timestamp': thought.timestamp.strftime('%d.%m.%Y, %H:%M'),
'branch': thought.branch,
'comments_count': len(thought.comments)
})
@app.route('/api/thoughts', methods=['POST'])
@login_required
def add_thought():
data = request.json
node_id = data.get('node_id')
content = data.get('content')
if not node_id or not content:
return jsonify({'error': 'Fehlende Daten'}), 400
node = MindMapNode.query.get_or_404(node_id)
thought = Thought(
content=content,
branch=node.name,
user_id=current_user.id
)
db.session.add(thought)
node.thoughts.append(thought)
db.session.commit()
return jsonify({
'id': thought.id,
'content': thought.content,
'author': thought.author.username,
'timestamp': thought.timestamp.strftime('%d.%m.%Y, %H:%M'),
'branch': thought.branch
})
@app.route('/api/comments/<int:thought_id>', methods=['GET'])
def get_comments(thought_id):
thought = Thought.query.get_or_404(thought_id)
comments = [
{
'id': comment.id,
'content': comment.content,
'author': comment.author.username,
'timestamp': comment.timestamp.strftime('%d.%m.%Y, %H:%M')
}
for comment in thought.comments
]
return jsonify(comments)
@app.route('/api/comments', methods=['POST'])
@login_required
def add_comment():
data = request.json
thought_id = data.get('thought_id')
content = data.get('content')
if not thought_id or not content:
return jsonify({'error': 'Fehlende Daten'}), 400
thought = Thought.query.get_or_404(thought_id)
comment = Comment(
content=content,
thought_id=thought_id,
user_id=current_user.id
)
db.session.add(comment)
db.session.commit()
return jsonify({
'id': comment.id,
'content': comment.content,
'author': comment.author.username,
'timestamp': comment.timestamp.strftime('%d.%m.%Y, %H:%M')
})
# Admin routes
@app.route('/admin')
@login_required
def admin():
if not current_user.is_admin:
flash('Zugriff verweigert')
return redirect(url_for('index'))
users = User.query.all()
nodes = MindMapNode.query.all()
thoughts = Thought.query.all()
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts)
# Flask starten
if __name__ == '__main__':
with app.app_context():
# Make sure tables exist
db.create_all()
app.run(host="0.0.0.0", debug=True)

88
website/init_db.py Normal file
View File

@@ -0,0 +1,88 @@
from app import app, db, User, MindMapNode
def init_database():
"""Initialize the database with admin user and mindmap structure."""
with app.app_context():
# Create all tables
db.create_all()
# Check if we already have users
if User.query.first() is None:
print("Creating admin user...")
# Create admin user
admin = User(username='admin', email='admin@example.com', is_admin=True)
admin.set_password('admin123')
db.session.add(admin)
# Create regular test user
test_user = User(username='test', email='test@example.com', is_admin=False)
test_user.set_password('test123')
db.session.add(test_user)
db.session.commit()
print("Admin user created successfully!")
# Check if we already have mindmap nodes
if MindMapNode.query.first() is None:
print("Creating initial mindmap structure...")
# Create initial mindmap structure
root = MindMapNode(name="Wissenschaftliche Mindmap")
db.session.add(root)
# Level 1 nodes
node1 = MindMapNode(name="Naturwissenschaften", parent=root)
node2 = MindMapNode(name="Geisteswissenschaften", parent=root)
node3 = MindMapNode(name="Technologie", parent=root)
node4 = MindMapNode(name="Künste", parent=root)
db.session.add_all([node1, node2, node3, node4])
# Level 2 nodes - Naturwissenschaften
node1_1 = MindMapNode(name="Physik", parent=node1)
node1_2 = MindMapNode(name="Biologie", parent=node1)
node1_3 = MindMapNode(name="Chemie", parent=node1)
node1_4 = MindMapNode(name="Astronomie", parent=node1)
db.session.add_all([node1_1, node1_2, node1_3, node1_4])
# Level 2 nodes - Geisteswissenschaften
node2_1 = MindMapNode(name="Philosophie", parent=node2)
node2_2 = MindMapNode(name="Geschichte", parent=node2)
node2_3 = MindMapNode(name="Psychologie", parent=node2)
node2_4 = MindMapNode(name="Soziologie", parent=node2)
db.session.add_all([node2_1, node2_2, node2_3, node2_4])
# Level 2 nodes - Technologie
node3_1 = MindMapNode(name="Informatik", parent=node3)
node3_2 = MindMapNode(name="Biotechnologie", parent=node3)
node3_3 = MindMapNode(name="Künstliche Intelligenz", parent=node3)
node3_4 = MindMapNode(name="Energietechnik", parent=node3)
db.session.add_all([node3_1, node3_2, node3_3, node3_4])
# Level 2 nodes - Künste
node4_1 = MindMapNode(name="Bildende Kunst", parent=node4)
node4_2 = MindMapNode(name="Musik", parent=node4)
node4_3 = MindMapNode(name="Literatur", parent=node4)
node4_4 = MindMapNode(name="Film", parent=node4)
db.session.add_all([node4_1, node4_2, node4_3, node4_4])
# Level 3 nodes - a few examples
# Physik
MindMapNode(name="Quantenphysik", parent=node1_1)
MindMapNode(name="Relativitätstheorie", parent=node1_1)
# Informatik
MindMapNode(name="Maschinelles Lernen", parent=node3_1)
MindMapNode(name="Softwareentwicklung", parent=node3_1)
MindMapNode(name="Datenbanken", parent=node3_1)
# Commit changes
db.session.commit()
print("Mindmap structure created successfully!")
print("Database initialization complete.")
if __name__ == "__main__":
init_database()
print("You can now run the application with 'python app.py'")
print("Login with:")
print(" Admin: username=admin, password=admin123")
print(" User: username=test, password=test123")

BIN
website/instance/mindmap.db Normal file

Binary file not shown.

6
website/requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
flask
flask-login
flask-wtf
email-validator
python-dotenv
flask-sqlalchemy

11
website/run.py Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python3
import os
from init_db import init_database
from app import app
if __name__ == "__main__":
# Initialize the database first
init_database()
# Run the Flask application
app.run(host="0.0.0.0", debug=True)

View File

@@ -0,0 +1,108 @@
// Background animation with Three.js
let scene, camera, renderer, stars = [];
function initBackground() {
// Setup scene
scene = new THREE.Scene();
// Setup camera
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.z = 100;
// Setup renderer
renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0); // Transparent background
// Append renderer to DOM
const backgroundContainer = document.getElementById('background-container');
if (backgroundContainer) {
backgroundContainer.appendChild(renderer.domElement);
}
// Add stars
for (let i = 0; i < 1000; i++) {
const geometry = new THREE.SphereGeometry(0.2, 8, 8);
const material = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: Math.random() * 0.5 + 0.1 });
const star = new THREE.Mesh(geometry, material);
// Random position
star.position.x = Math.random() * 600 - 300;
star.position.y = Math.random() * 600 - 300;
star.position.z = Math.random() * 600 - 300;
// Store reference to move in animation
star.velocity = Math.random() * 0.02 + 0.005;
stars.push(star);
scene.add(star);
}
// Add large glowing particles
for (let i = 0; i < 15; i++) {
const size = Math.random() * 5 + 2;
const geometry = new THREE.SphereGeometry(size, 16, 16);
// Create a glowing material
const color = new THREE.Color();
color.setHSL(Math.random(), 0.7, 0.5); // Random hue
const material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.2
});
const particle = new THREE.Mesh(geometry, material);
// Random position but further away
particle.position.x = Math.random() * 1000 - 500;
particle.position.y = Math.random() * 1000 - 500;
particle.position.z = Math.random() * 200 - 400;
// Store reference to move in animation
particle.velocity = Math.random() * 0.01 + 0.002;
stars.push(particle);
scene.add(particle);
}
// Handle window resize
window.addEventListener('resize', onWindowResize);
// Start animation
animate();
}
function animate() {
requestAnimationFrame(animate);
// Move stars
stars.forEach(star => {
star.position.z += star.velocity;
// Reset position if star moves too close
if (star.position.z > 100) {
star.position.z = -300;
}
});
// Rotate the entire scene slightly for a dreamy effect
scene.rotation.y += 0.0003;
scene.rotation.x += 0.0001;
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Initialize background when the DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBackground);
} else {
initBackground();
}

View File

@@ -0,0 +1,109 @@
{% extends "base.html" %}
{% block title %}Admin | Wissenschaftliche Mindmap{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="glass p-8 mb-8">
<h1 class="text-3xl font-bold text-white mb-4">Admin Bereich</h1>
<p class="text-white/70">Verwalte Benutzer, Gedanken und die Mindmap-Struktur.</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Users Section -->
<div class="dark-glass p-6" x-data="{ tab: 'users' }">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white">Benutzer</h2>
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ users|length }}</span>
</div>
<div class="overflow-y-auto max-h-[500px]">
<table class="w-full text-white/90">
<thead class="text-white/60 text-sm uppercase">
<tr>
<th class="text-left py-3">ID</th>
<th class="text-left py-3">Benutzername</th>
<th class="text-left py-3">Email</th>
<th class="text-left py-3">Rolle</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr class="border-t border-white/10">
<td class="py-3">{{ user.id }}</td>
<td class="py-3">{{ user.username }}</td>
<td class="py-3">{{ user.email }}</td>
<td class="py-3">
{% if user.is_admin %}
<span class="bg-purple-600/70 text-white text-xs px-2 py-1 rounded">Admin</span>
{% else %}
<span class="bg-blue-600/70 text-white text-xs px-2 py-1 rounded">Benutzer</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Mindmap Nodes Section -->
<div class="dark-glass p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white">Mindmap Struktur</h2>
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ nodes|length }}</span>
</div>
<div class="overflow-y-auto max-h-[500px]">
<div class="space-y-3">
{% for node in nodes %}
<div class="glass p-3">
<div class="flex justify-between items-center">
<span class="font-medium">{{ node.name }}</span>
<span class="text-xs text-white/60">ID: {{ node.id }}</span>
</div>
{% if node.parent %}
<p class="text-sm text-white/60 mt-1">Eltern: {{ node.parent.name }}</p>
{% else %}
<p class="text-sm text-white/60 mt-1">Hauptknoten</p>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<div class="mt-6">
<button class="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-4 py-2 rounded-lg transition-all duration-300 text-sm w-full">
Neuen Knoten hinzufügen
</button>
</div>
</div>
<!-- Thoughts Section -->
<div class="dark-glass p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white">Gedanken</h2>
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ thoughts|length }}</span>
</div>
<div class="overflow-y-auto max-h-[500px]">
<div class="space-y-3">
{% for thought in thoughts %}
<div class="glass p-3">
<div class="flex justify-between items-start">
<span class="inline-block px-2 py-0.5 text-xs text-white/70 bg-white/10 rounded-full mb-1">{{ thought.branch }}</span>
<span class="text-xs text-white/50">{{ thought.timestamp.strftime('%d.%m.%Y') }}</span>
</div>
<p class="text-sm text-white mb-1 line-clamp-2">{{ thought.content }}</p>
<div class="flex justify-between items-center mt-2 text-xs">
<span class="text-white/60">Von: {{ thought.author.username }}</span>
<span class="text-white/60">{{ thought.comments|length }} Kommentar(e)</span>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

126
website/templates/base.html Normal file
View File

@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Wissenschaftliche Mindmap{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#10b981',
accent: '#8b5cf6',
},
fontFamily: {
sans: ['Poppins', 'sans-serif'],
},
},
},
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
.glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
}
.dark-glass {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
body {
min-height: 100vh;
background-color: #050b14;
font-family: 'Poppins', sans-serif;
position: relative;
overflow-x: hidden;
}
.gradient-text {
background-clip: text;
-webkit-background-clip: text;
color: transparent;
background-image: linear-gradient(to right, #4f46e5, #8b5cf6);
}
#background-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
}
#background-container canvas {
position: absolute;
top: 0;
left: 0;
}
</style>
{% block extra_head %}{% endblock %}
</head>
<body class="antialiased text-gray-100">
<!-- Animated background container -->
<div id="background-container"></div>
<div class="min-h-screen">
<!-- Navigation -->
<nav class="glass px-4 py-3 mx-4 mt-4 flex justify-between items-center">
<a href="{{ url_for('index') }}" class="text-white text-xl font-bold">MindMap</a>
<div class="flex space-x-4">
<a href="{{ url_for('mindmap') }}" class="text-white hover:text-indigo-200 transition-colors">Mindmap</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('profile') }}" class="text-white hover:text-indigo-200 transition-colors">Profil</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin') }}" class="text-white hover:text-indigo-200 transition-colors">Admin</a>
{% endif %}
<a href="{{ url_for('logout') }}" class="text-white hover:text-indigo-200 transition-colors">Abmelden</a>
{% else %}
<a href="{{ url_for('login') }}" class="text-white hover:text-indigo-200 transition-colors">Anmelden</a>
<a href="{{ url_for('register') }}" class="text-white hover:text-indigo-200 transition-colors">Registrieren</a>
{% endif %}
</div>
</nav>
<!-- Flash messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="container mx-auto mt-4 px-4">
{% for message in messages %}
<div class="glass p-4 mb-4 text-white">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Main content -->
<main class="container mx-auto p-4">
{% block content %}{% endblock %}
</main>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="{{ url_for('static', filename='background.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -1,18 +1,41 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Wissenschaftliche Mindmap</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body class="dark-theme">
<video autoplay muted loop id="bg-video">
<source src="{{ url_for('static', filename='background.mp4') }}" type="video/mp4">
</video>
<div class="overlay">
<h1>Willkommen zur Wissenschafts-Mindmap</h1>
<p>Verknüpfe Wissen in neuronalen Strukturen.</p>
<a href="{{ url_for('mindmap') }}" class="cta-button">Starte die Mindmap</a>
{% extends "base.html" %}
{% block content %}
<div class="flex flex-col items-center justify-center min-h-[80vh] text-center">
<div class="glass p-8 max-w-2xl w-full">
<h1 class="text-4xl font-bold text-white mb-4">Willkommen zur Wissenschafts-Mindmap</h1>
<p class="text-xl text-white/80 mb-8">Verknüpfe Wissen in neuronalen Strukturen und teile deine Gedanken mit der Community.</p>
<div class="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4 justify-center">
<a href="{{ url_for('mindmap') }}" class="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300 transform hover:scale-105">
Starte die Mindmap
</a>
{% if not current_user.is_authenticated %}
<a href="{{ url_for('register') }}" class="glass hover:bg-white/20 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300 transform hover:scale-105">
Registrieren
</a>
{% endif %}
</div>
</div>
</body>
</html>
<div class="mt-12 grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-4xl">
<div class="glass p-6 text-white">
<div class="text-3xl mb-2">🧠</div>
<h3 class="text-xl font-semibold mb-2">Visualisiere Wissen</h3>
<p class="text-white/80">Erkenne Zusammenhänge zwischen verschiedenen Wissensgebieten durch intuitive Mindmaps.</p>
</div>
<div class="glass p-6 text-white">
<div class="text-3xl mb-2">💡</div>
<h3 class="text-xl font-semibold mb-2">Teile Gedanken</h3>
<p class="text-white/80">Füge deine eigenen Gedanken zu bestehenden Themen hinzu und bereichere die Community.</p>
</div>
<div class="glass p-6 text-white">
<div class="text-3xl mb-2">🔄</div>
<h3 class="text-xl font-semibold mb-2">Interaktive Vernetzung</h3>
<p class="text-white/80">Beteilige dich an Diskussionen und sieh wie sich Ideen gemeinsam entwickeln.</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}Anmelden | Wissenschaftliche Mindmap{% endblock %}
{% block content %}
<div class="flex justify-center items-center min-h-[80vh]">
<div class="glass p-8 w-full max-w-md">
<h1 class="text-3xl font-bold text-white mb-6 text-center">Anmelden</h1>
<form method="POST" action="{{ url_for('login') }}" class="space-y-6">
<div>
<label for="username" class="block text-sm font-medium text-white mb-1">Benutzername</label>
<input type="text" id="username" name="username" required
class="w-full px-4 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">
</div>
<div>
<label for="password" class="block text-sm font-medium text-white mb-1">Passwort</label>
<input type="password" id="password" name="password" required
class="w-full px-4 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">
</div>
<div>
<button type="submit"
class="w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300">
Anmelden
</button>
</div>
</form>
<div class="mt-6 text-center">
<p class="text-white/70">
Noch kein Konto?
<a href="{{ url_for('register') }}" class="text-indigo-300 hover:text-white font-medium transition-colors">
Registrieren
</a>
</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -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 %}

View File

@@ -0,0 +1,131 @@
{% extends "base.html" %}
{% block title %}Profil | Wissenschaftliche Mindmap{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="glass p-8 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
<div>
<h1 class="text-3xl font-bold text-white">Hallo, {{ current_user.username }}</h1>
<p class="text-white/70 mt-1">{{ current_user.email }}</p>
</div>
<div class="mt-4 md:mt-0">
<a href="{{ url_for('mindmap') }}"
class="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300 inline-block">
Zur Mindmap
</a>
</div>
</div>
</div>
<div class="dark-glass p-8">
<h2 class="text-2xl font-bold text-white mb-6">Deine Gedanken</h2>
{% if thoughts %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for thought in thoughts %}
<div class="glass p-6 hover:shadow-lg transition-all" x-data="{ showActions: false }" @mouseenter="showActions = true" @mouseleave="showActions = false">
<div class="flex justify-between items-start">
<span class="inline-block px-3 py-1 text-xs text-white/70 bg-white/10 rounded-full mb-3">{{ thought.branch }}</span>
<span class="text-xs text-white/50">{{ thought.timestamp.strftime('%d.%m.%Y, %H:%M') }}</span>
</div>
<p class="text-white mb-4 leading-relaxed">{{ thought.content }}</p>
<div class="flex justify-between items-center" x-show="showActions" x-transition.opacity>
<div class="text-xs text-white/70">
<span>{{ thought.comments|length }} Kommentar(e)</span>
</div>
<a href="#" onclick="openThoughtDetails('{{ thought.id }}')" class="text-indigo-300 hover:text-white text-sm">Details anzeigen</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-12">
<p class="text-white/70 mb-4">Du hast noch keine Gedanken geteilt.</p>
<a href="{{ url_for('mindmap') }}" class="text-indigo-300 hover:text-white">Zur Mindmap gehen und mitmachen</a>
</div>
{% endif %}
</div>
</div>
<!-- Thought Detail Modal -->
<div id="thoughtModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 hidden">
<div class="dark-glass p-8 w-full max-w-2xl max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-bold text-white" id="modalThoughtTitle">Gedanke Details</h3>
<button onclick="closeThoughtModal()" 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 id="modalContent" class="space-y-6">
<div class="glass p-4">
<div class="flex justify-between items-start mb-2">
<span class="inline-block px-3 py-1 text-xs text-white/70 bg-white/10 rounded-full" id="modalBranch"></span>
<span class="text-xs text-white/50" id="modalTimestamp"></span>
</div>
<p class="text-white" id="modalThoughtContent"></p>
</div>
<div>
<h4 class="text-lg font-medium text-white mb-3">Kommentare</h4>
<div id="commentsList" class="space-y-3"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function openThoughtDetails(thoughtId) {
fetch(`/api/thoughts/${thoughtId}`)
.then(response => response.json())
.then(thought => {
document.getElementById('modalThoughtTitle').textContent = `Gedanke von ${thought.author}`;
document.getElementById('modalBranch').textContent = thought.branch;
document.getElementById('modalTimestamp').textContent = thought.timestamp;
document.getElementById('modalThoughtContent').textContent = thought.content;
// Load comments
return fetch(`/api/comments/${thoughtId}`);
})
.then(response => response.json())
.then(comments => {
const commentsList = document.getElementById('commentsList');
commentsList.innerHTML = '';
if (comments.length === 0) {
commentsList.innerHTML = '<p class="text-white/50">Keine Kommentare vorhanden</p>';
return;
}
comments.forEach(comment => {
const commentEl = document.createElement('div');
commentEl.className = 'glass p-3';
commentEl.innerHTML = `
<div class="flex justify-between items-start mb-1">
<span class="text-sm font-medium text-white">${comment.author}</span>
<span class="text-xs text-white/50">${comment.timestamp}</span>
</div>
<p class="text-white/90 text-sm">${comment.content}</p>
`;
commentsList.appendChild(commentEl);
});
})
.catch(error => console.error('Error loading thought details:', error));
document.getElementById('thoughtModal').classList.remove('hidden');
}
function closeThoughtModal() {
document.getElementById('thoughtModal').classList.add('hidden');
}
</script>
{% endblock %}

View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Registrieren | Wissenschaftliche Mindmap{% endblock %}
{% block content %}
<div class="flex justify-center items-center min-h-[80vh]">
<div class="glass p-8 w-full max-w-md">
<h1 class="text-3xl font-bold text-white mb-6 text-center">Registrieren</h1>
<form method="POST" action="{{ url_for('register') }}" class="space-y-6">
<div>
<label for="username" class="block text-sm font-medium text-white mb-1">Benutzername</label>
<input type="text" id="username" name="username" required
class="w-full px-4 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">
</div>
<div>
<label for="email" class="block text-sm font-medium text-white mb-1">E-Mail</label>
<input type="email" id="email" name="email" required
class="w-full px-4 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">
</div>
<div>
<label for="password" class="block text-sm font-medium text-white mb-1">Passwort</label>
<input type="password" id="password" name="password" required
class="w-full px-4 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">
</div>
<div>
<button type="submit"
class="w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300">
Registrieren
</button>
</div>
</form>
<div class="mt-6 text-center">
<p class="text-white/70">
Bereits registriert?
<a href="{{ url_for('login') }}" class="text-indigo-300 hover:text-white font-medium transition-colors">
Anmelden
</a>
</p>
</div>
</div>
</div>
{% endblock %}