Compare commits
103 Commits
tills-bran
...
40b28134fc
| Author | SHA1 | Date | |
|---|---|---|---|
| 40b28134fc | |||
| d5fababd49 | |||
| 7c742debdf | |||
| 4a4271a23c | |||
| c1038b479f | |||
| cd0083544a | |||
| a03bec2dff | |||
| 997479581d | |||
| 8153390e35 | |||
| bfa155628e | |||
| 700a8a3b89 | |||
| 808481ffe7 | |||
| e2c8cfaacf | |||
| 78e37fa717 | |||
| b2cf50626a | |||
| 7f48526315 | |||
| 84f8a6bf31 | |||
| 7003c89447 | |||
| d0821db983 | |||
| f0c4c514c4 | |||
| 304a399b85 | |||
| a5396c0d6e | |||
| 9cc4e70cba | |||
| a8cac08d30 | |||
| 42a7485ce1 | |||
| 54a5ccc224 | |||
| a99f82d4cf | |||
| 699127f41f | |||
| e8d356a27a | |||
| daf2704253 | |||
| 084059449f | |||
| c9bbc6ff25 | |||
| 742e3fda20 | |||
| 54aa246b79 | |||
| 505fb9aa47 | |||
| e4e6541b8c | |||
| e724181915 | |||
| 460c3f987e | |||
| 7f33dea278 | |||
| 726d9c9c70 | |||
| 81170fbd3d | |||
| eff3fda1ca | |||
| d49b266d96 | |||
| 34a08c4a6a | |||
| 7918de1723 | |||
| a0e4cd2208 | |||
| 2199d6007c | |||
| 7fb9452d09 | |||
| 1f3e60efde | |||
| 5e97381c8f | |||
| 4c402423c0 | |||
| 6d2595e3a6 | |||
| 29b44e5c52 | |||
| 693e542d5f | |||
| 4c3e476338 | |||
| 613c38ccb2 | |||
| 91fdd43fe0 | |||
| f36dd5ffaa | |||
| 2e1c3ce8b0 | |||
| d80c4c9aec | |||
| 3b0bea959c | |||
| cb3bfe0e6a | |||
| fd63810845 | |||
| 883973fe7b | |||
| 027e632856 | |||
| 406289e54f | |||
| 71b33e6cec | |||
| c74d3164bb | |||
| 4982cddeef | |||
| 631619ccb4 | |||
| f9881b678d | |||
| 259ce3cf69 | |||
| 9f4743eaea | |||
| de0f837cfd | |||
| 1c49ddfb19 | |||
| 46c16e5f01 | |||
| 84667bca00 | |||
| 779449559d | |||
| 721a10e861 | |||
| a431873ca2 | |||
| e4ab1e1bb5 | |||
| f69356473b | |||
| 38ac13e87c | |||
| 0afb8cb6e2 | |||
| 5d282d2108 | |||
| 4aba72efa2 | |||
| 89476d5353 | |||
| 0f7a33340a | |||
| 73501e7cda | |||
| 9f8eba6736 | |||
| b6bf9f387d | |||
| d9fe1f8efc | |||
| fd7bc59851 | |||
| 55f1f87509 | |||
| 03f8761312 | |||
| 506748fda7 | |||
| 6d069f68cd | |||
| 4310239a7a | |||
| e9fe907af0 | |||
| 0c69d9aba3 | |||
| 6da85cdece | |||
| a073b09115 | |||
| f1f4870989 |
17
.env
17
.env
@@ -1,2 +1,15 @@
|
|||||||
SECRET_KEY=eed9298856dc9363cd32778265780d6904ba24e6a6b815a2cc382bcdd767ea7b
|
# MindMap Umgebungsvariablen
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
FLASK_APP=app.py
|
||||||
|
FLASK_DEBUG=1
|
||||||
|
SECRET_KEY=your-secret-key-replace-in-production
|
||||||
|
|
||||||
|
# OpenAI API
|
||||||
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
|
# Datenbank
|
||||||
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
|
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Dockerfile
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Arbeitsverzeichnis in Container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends gcc && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
mkdir -p /app/database
|
||||||
|
|
||||||
|
# pip auf den neuesten Stand bringen und Requirements installieren
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -U -r requirements.txt
|
||||||
|
|
||||||
|
# Anwendungscode kopieren
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Berechtigungen für database-Ordner
|
||||||
|
RUN chmod -R 777 /app/database
|
||||||
|
|
||||||
|
# Exponiere Port 5000 für Flask
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Setze Umgebungsvariablen
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# Startkommando mit spezifischen Flags für Produktion
|
||||||
|
CMD ["python", "app.py"]
|
||||||
Binary file not shown.
Binary file not shown.
749
app.py
749
app.py
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, g
|
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, g
|
||||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
@@ -19,6 +19,7 @@ from openai import OpenAI
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from flask_socketio import SocketIO, emit
|
from flask_socketio import SocketIO, emit
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
# Modelle importieren
|
# Modelle importieren
|
||||||
from models import (
|
from models import (
|
||||||
@@ -45,10 +46,10 @@ app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(
|
|||||||
app.config['WTF_CSRF_ENABLED'] = False
|
app.config['WTF_CSRF_ENABLED'] = False
|
||||||
|
|
||||||
# OpenAI API-Konfiguration
|
# OpenAI API-Konfiguration
|
||||||
api_key = os.environ.get("OPENAI_API_KEY")
|
api_key = "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"
|
||||||
if not api_key:
|
# api_key = os.environ.get("OPENAI_API_KEY")
|
||||||
print("WARNUNG: Kein OPENAI_API_KEY in Umgebungsvariablen gefunden. KI-Funktionalität wird nicht verfügbar sein.")
|
# if not api_key:
|
||||||
api_key = "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"
|
# print("WARNUNG: Kein OPENAI_API_KEY in Umgebungsvariablen gefunden. KI-Funktionalität wird nicht verfügbar sein.")
|
||||||
|
|
||||||
client = OpenAI(api_key=api_key)
|
client = OpenAI(api_key=api_key)
|
||||||
|
|
||||||
@@ -198,97 +199,31 @@ def initialize_database():
|
|||||||
# Erstelle alle Tabellen
|
# Erstelle alle Tabellen
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
# Prüfe, ob bereits Benutzer existieren
|
# Prüfen, ob bereits Kategorien existieren
|
||||||
if User.query.count() == 0:
|
categories_count = Category.query.count()
|
||||||
print("Erstelle Admin-Benutzer...")
|
users_count = User.query.count()
|
||||||
admin = User(
|
|
||||||
username="admin",
|
|
||||||
email="admin@example.com",
|
|
||||||
is_admin=True
|
|
||||||
)
|
|
||||||
admin.set_password("admin123") # In echter Umgebung ein sicheres Passwort verwenden!
|
|
||||||
db.session.add(admin)
|
|
||||||
|
|
||||||
# Prüfe, ob bereits Kategorien existieren
|
# Erstelle Standarddaten, wenn es keine Kategorien gibt
|
||||||
if Category.query.count() == 0:
|
if categories_count == 0:
|
||||||
print("Erstelle Standard-Kategorien...")
|
|
||||||
create_default_categories()
|
create_default_categories()
|
||||||
|
|
||||||
# Stelle sicher, dass die Standard-Knoten für die öffentliche Mindmap existieren
|
# Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind
|
||||||
if MindMapNode.query.count() == 0:
|
if users_count == 0:
|
||||||
print("Erstelle Standard-Knoten für die Mindmap...")
|
admin_user = User(
|
||||||
|
username="admin",
|
||||||
# Hauptknoten: Wissen
|
email="admin@example.com",
|
||||||
root_node = MindMapNode(
|
role="admin",
|
||||||
name="Wissen",
|
is_active=True
|
||||||
description="Zentrale Wissensbasis",
|
|
||||||
color_code="#4299E1",
|
|
||||||
is_public=True
|
|
||||||
)
|
)
|
||||||
db.session.add(root_node)
|
admin_user.set_password("admin123") # Sicheres Passwort in der Produktion verwenden!
|
||||||
db.session.flush() # Um die ID zu generieren
|
db.session.add(admin_user)
|
||||||
|
|
||||||
# Verwandte Kategorien finden
|
|
||||||
philosophy = Category.query.filter_by(name="Philosophie").first()
|
|
||||||
science = Category.query.filter_by(name="Wissenschaft").first()
|
|
||||||
technology = Category.query.filter_by(name="Technologie").first()
|
|
||||||
arts = Category.query.filter_by(name="Künste").first()
|
|
||||||
|
|
||||||
# Erstelle Hauptthemenknoten
|
|
||||||
nodes = [
|
|
||||||
MindMapNode(
|
|
||||||
name="Philosophie",
|
|
||||||
description="Philosophisches Denken",
|
|
||||||
color_code="#9F7AEA",
|
|
||||||
category=philosophy,
|
|
||||||
is_public=True
|
|
||||||
),
|
|
||||||
MindMapNode(
|
|
||||||
name="Wissenschaft",
|
|
||||||
description="Wissenschaftliche Erkenntnisse",
|
|
||||||
color_code="#48BB78",
|
|
||||||
category=science,
|
|
||||||
is_public=True
|
|
||||||
),
|
|
||||||
MindMapNode(
|
|
||||||
name="Technologie",
|
|
||||||
description="Technologische Entwicklungen",
|
|
||||||
color_code="#ED8936",
|
|
||||||
category=technology,
|
|
||||||
is_public=True
|
|
||||||
),
|
|
||||||
MindMapNode(
|
|
||||||
name="Künste",
|
|
||||||
description="Künstlerische Ausdrucksformen",
|
|
||||||
color_code="#ED64A6",
|
|
||||||
category=arts,
|
|
||||||
is_public=True
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Füge Knoten zur Datenbank hinzu
|
|
||||||
for node in nodes:
|
|
||||||
db.session.add(node)
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
print("Admin-Benutzer wurde erstellt!")
|
||||||
# Nachdem wir die IDs haben, füge die Verbindungen hinzu
|
|
||||||
all_nodes = MindMapNode.query.all()
|
return True
|
||||||
root = MindMapNode.query.filter_by(name="Wissen").first()
|
|
||||||
|
|
||||||
if root:
|
|
||||||
for node in all_nodes:
|
|
||||||
if node.id != root.id:
|
|
||||||
root.children.append(node)
|
|
||||||
|
|
||||||
# Speichere die Änderungen
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
print("Datenbankinitialisierung abgeschlossen.")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fehler bei der Datenbankinitialisierung: {str(e)}")
|
print(f"Fehler bei Datenbank-Initialisierung: {e}")
|
||||||
db.session.rollback()
|
return False
|
||||||
raise
|
|
||||||
|
|
||||||
# Instead of before_first_request, which is deprecated in newer Flask versions
|
# Instead of before_first_request, which is deprecated in newer Flask versions
|
||||||
# Use a function to initialize the database that will be called during app creation
|
# Use a function to initialize the database that will be called during app creation
|
||||||
@@ -315,7 +250,8 @@ def admin_required(f):
|
|||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(id):
|
def load_user(id):
|
||||||
return User.query.get(int(id))
|
# Verwende session.get() anstelle von query.get() für SQLAlchemy 2.0 Kompatibilität
|
||||||
|
return db.session.get(User, int(id))
|
||||||
|
|
||||||
# Routes for authentication
|
# Routes for authentication
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
@@ -325,14 +261,16 @@ def login():
|
|||||||
password = request.form.get('password')
|
password = request.form.get('password')
|
||||||
|
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
|
|
||||||
if user and user.check_password(password):
|
if user and user.check_password(password):
|
||||||
login_user(user)
|
login_user(user)
|
||||||
# Aktualisiere letzten Login-Zeitpunkt
|
# Aktualisiere letzten Login-Zeitpunkt
|
||||||
user.last_login = datetime.utcnow()
|
user.last_login = datetime.now(timezone.utc)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
next_page = request.args.get('next')
|
next_page = request.args.get('next')
|
||||||
return redirect(next_page or url_for('index'))
|
return redirect(next_page or url_for('index'))
|
||||||
|
|
||||||
flash('Ungültiger Benutzername oder Passwort')
|
flash('Ungültiger Benutzername oder Passwort')
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
|
|
||||||
@@ -354,15 +292,21 @@ def register():
|
|||||||
user = User(username=username, email=email)
|
user = User(username=username, email=email)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
|
db.session.commit() # Commit, um eine ID für den Benutzer zu erhalten
|
||||||
|
|
||||||
# Erstelle eine Standard-Mindmap für den neuen Benutzer
|
# Erstelle eine Standard-Mindmap für den neuen Benutzer
|
||||||
default_mindmap = UserMindmap(
|
try:
|
||||||
name='Meine Mindmap',
|
default_mindmap = UserMindmap(
|
||||||
description='Meine persönliche Wissenslandschaft',
|
name='Meine Mindmap',
|
||||||
user=user
|
description='Meine persönliche Wissenslandschaft',
|
||||||
)
|
user_id=user.id
|
||||||
db.session.add(default_mindmap)
|
)
|
||||||
db.session.commit()
|
db.session.add(default_mindmap)
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Erstellen der Standard-Mindmap: {e}")
|
||||||
|
# Stelle sicher, dass wir trotzdem weitermachen können
|
||||||
|
db.session.rollback()
|
||||||
|
|
||||||
login_user(user)
|
login_user(user)
|
||||||
flash('Dein Konto wurde erfolgreich erstellt!', 'success')
|
flash('Dein Konto wurde erfolgreich erstellt!', 'success')
|
||||||
@@ -383,71 +327,152 @@ def index():
|
|||||||
# Route for the mindmap page
|
# Route for the mindmap page
|
||||||
@app.route('/mindmap')
|
@app.route('/mindmap')
|
||||||
def mindmap():
|
def mindmap():
|
||||||
"""Zeigt die öffentliche Mindmap an."""
|
"""Zeigt die Mindmap-Seite an."""
|
||||||
try:
|
|
||||||
# Sicherstellen, dass wir Kategorien haben
|
# Benutzer-Mindmaps, falls angemeldet
|
||||||
if Category.query.count() == 0:
|
user_mindmaps = []
|
||||||
create_default_categories()
|
if current_user.is_authenticated:
|
||||||
|
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||||
# Hole alle Kategorien der obersten Ebene
|
|
||||||
categories = Category.query.filter_by(parent_id=None).all()
|
# Stelle sicher, dass der "Wissen"-Knoten existiert
|
||||||
|
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
|
||||||
# Transformiere Kategorien in ein anzeigbares Format für die Vorlage
|
if not wissen_node:
|
||||||
category_tree = [build_category_tree(cat) for cat in categories]
|
wissen_node = MindMapNode(
|
||||||
|
name="Wissen",
|
||||||
return render_template('mindmap.html', categories=category_tree)
|
description="Zentrale Wissensbasis",
|
||||||
except Exception as e:
|
color_code="#4299E1",
|
||||||
# Bei Fehler leere Kategorienliste übergeben und Fehler protokollieren
|
is_public=True
|
||||||
print(f"Fehler beim Laden der Mindmap-Kategorien: {str(e)}")
|
)
|
||||||
return render_template('mindmap.html', categories=[], error=str(e))
|
db.session.add(wissen_node)
|
||||||
|
db.session.commit()
|
||||||
|
print("'Wissen'-Knoten wurde erstellt")
|
||||||
|
|
||||||
|
# Überprüfe, ob es Kategorien gibt, sonst erstelle sie
|
||||||
|
if Category.query.count() == 0:
|
||||||
|
create_default_categories()
|
||||||
|
print("Kategorien wurden erstellt")
|
||||||
|
|
||||||
|
# Stelle sicher, dass die Route für statische Dateien korrekt ist
|
||||||
|
mindmap_js_path = url_for('static', filename='js/mindmap-init.js')
|
||||||
|
|
||||||
|
return render_template('mindmap.html', user_mindmaps=user_mindmaps, mindmap_js_path=mindmap_js_path)
|
||||||
|
|
||||||
# Route for user profile
|
# Route for user profile
|
||||||
@app.route('/profile')
|
@app.route('/profile')
|
||||||
@login_required
|
@login_required
|
||||||
def profile():
|
def profile():
|
||||||
# Lade Benutzer-Mindmaps
|
try:
|
||||||
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
# Versuche auf die neue Benutzermodellstruktur zuzugreifen
|
||||||
|
_ = current_user.bio # Dies wird fehlschlagen, wenn die Spalte nicht existiert
|
||||||
# Lade Statistiken
|
|
||||||
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
|
# Wenn keine Ausnahme, fahre mit normalem Profil fort
|
||||||
bookmark_count = db.session.query(user_thought_bookmark).filter(
|
# Lade Benutzer-Mindmaps
|
||||||
user_thought_bookmark.c.user_id == current_user.id).count()
|
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||||
|
|
||||||
# Berechne tatsächliche Werte für Benutzerstatistiken
|
# Prüfe, ob der Benutzer eine Standard-Mindmap hat, sonst erstelle eine
|
||||||
contributions_count = Comment.query.filter_by(user_id=current_user.id).count()
|
if not user_mindmaps:
|
||||||
|
try:
|
||||||
# Berechne Verbindungen (Anzahl der Gedankenverknüpfungen)
|
default_mindmap = UserMindmap(
|
||||||
connections_count = ThoughtRelation.query.filter(
|
name='Meine Mindmap',
|
||||||
(ThoughtRelation.source_id.in_(
|
description='Meine persönliche Wissenslandschaft',
|
||||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
user_id=current_user.id
|
||||||
)) |
|
)
|
||||||
(ThoughtRelation.target_id.in_(
|
db.session.add(default_mindmap)
|
||||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
db.session.commit()
|
||||||
))
|
|
||||||
).count()
|
# Aktualisiere die Liste nach dem Erstellen
|
||||||
|
user_mindmaps = [default_mindmap]
|
||||||
# Berechne durchschnittliche Bewertung der Gedanken des Benutzers
|
except Exception as e:
|
||||||
avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).join(
|
print(f"Fehler beim Erstellen der Standard-Mindmap in Profil: {e}")
|
||||||
Thought, Thought.id == ThoughtRating.thought_id
|
# Flash-Nachricht für den Benutzer
|
||||||
).filter(Thought.user_id == current_user.id).scalar() or 0
|
flash('Es gab ein Problem beim Laden deiner Mindmaps. Bitte versuche es später erneut.', 'warning')
|
||||||
|
|
||||||
# Hole die Anzahl der Follower (falls implementiert)
|
# Lade Statistiken
|
||||||
# In diesem Beispiel nehmen wir an, dass es keine Follower-Funktionalität gibt
|
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
|
||||||
followers_count = 0
|
bookmark_count = db.session.query(user_thought_bookmark).filter(
|
||||||
|
user_thought_bookmark.c.user_id == current_user.id).count()
|
||||||
# Hole den Standort des Benutzers aus der Datenbank, falls vorhanden
|
|
||||||
location = "Deutschland" # Standardwert
|
# Berechne tatsächliche Werte für Benutzerstatistiken
|
||||||
|
contributions_count = Comment.query.filter_by(user_id=current_user.id).count()
|
||||||
return render_template('profile.html',
|
|
||||||
user=current_user,
|
# Berechne Verbindungen (Anzahl der Gedankenverknüpfungen)
|
||||||
user_mindmaps=user_mindmaps,
|
connections_count = ThoughtRelation.query.filter(
|
||||||
thought_count=thought_count,
|
(ThoughtRelation.source_id.in_(
|
||||||
bookmark_count=bookmark_count,
|
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||||
connections_count=connections_count,
|
)) |
|
||||||
contributions_count=contributions_count,
|
(ThoughtRelation.target_id.in_(
|
||||||
followers_count=followers_count,
|
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||||
rating=round(avg_rating, 1),
|
))
|
||||||
location=location)
|
).count()
|
||||||
|
|
||||||
|
# Berechne durchschnittliche Bewertung der Gedanken des Benutzers
|
||||||
|
avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).join(
|
||||||
|
Thought, Thought.id == ThoughtRating.thought_id
|
||||||
|
).filter(Thought.user_id == current_user.id).scalar() or 0
|
||||||
|
|
||||||
|
# Sammle alle Statistiken in einem Wörterbuch
|
||||||
|
stats = {
|
||||||
|
'thought_count': thought_count,
|
||||||
|
'bookmark_count': bookmark_count,
|
||||||
|
'connections_count': connections_count,
|
||||||
|
'contributions_count': contributions_count,
|
||||||
|
'followers_count': 0, # Platzhalter für zukünftige Funktionalität
|
||||||
|
'rating': round(avg_rating, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hole die letzten Gedanken des Benutzers
|
||||||
|
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.created_at.desc()).limit(5).all()
|
||||||
|
|
||||||
|
# Hole den Standort des Benutzers aus der Datenbank, falls vorhanden
|
||||||
|
location = "Deutschland" # Standardwert
|
||||||
|
|
||||||
|
return render_template('profile.html',
|
||||||
|
user=current_user,
|
||||||
|
user_mindmaps=user_mindmaps,
|
||||||
|
stats=stats,
|
||||||
|
thoughts=thoughts,
|
||||||
|
location=location)
|
||||||
|
|
||||||
|
except (AttributeError, sqlalchemy.exc.OperationalError) as e:
|
||||||
|
# Die Spalte existiert nicht, verwende stattdessen das einfache Profil
|
||||||
|
print(f"Verwende einfaches Profil wegen Datenbankfehler: {e}")
|
||||||
|
flash('Dein Profil wird im einfachen Modus angezeigt, bis die Datenbank aktualisiert wird.', 'warning')
|
||||||
|
|
||||||
|
# Lade nur die grundlegenden Informationen
|
||||||
|
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||||
|
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.created_at.desc()).limit(5).all()
|
||||||
|
|
||||||
|
return render_template('simple_profile.html',
|
||||||
|
user=current_user,
|
||||||
|
user_mindmaps=user_mindmaps,
|
||||||
|
thoughts=thoughts)
|
||||||
|
except Exception as e:
|
||||||
|
# Eine andere Ausnahme ist aufgetreten
|
||||||
|
print(f"Fehler beim Laden des Profils: {e}")
|
||||||
|
flash('Beim Laden Ihres Benutzerprofils ist ein Fehler aufgetreten. Wir zeigen Ihnen die Standard-Ansicht.', 'error')
|
||||||
|
|
||||||
|
# Erstelle grundlegende stats
|
||||||
|
stats = {
|
||||||
|
'thought_count': 0,
|
||||||
|
'bookmark_count': 0,
|
||||||
|
'connections_count': 0,
|
||||||
|
'contributions_count': 0,
|
||||||
|
'followers_count': 0,
|
||||||
|
'rating': 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leere Listen für Mindmaps und Gedanken
|
||||||
|
user_mindmaps = []
|
||||||
|
thoughts = []
|
||||||
|
location = "Deutschland"
|
||||||
|
|
||||||
|
# Zeige das normale Profil mit minimalen Daten an
|
||||||
|
return render_template('profile.html',
|
||||||
|
user=current_user,
|
||||||
|
user_mindmaps=user_mindmaps,
|
||||||
|
stats=stats,
|
||||||
|
thoughts=thoughts,
|
||||||
|
location=location)
|
||||||
|
|
||||||
# Route für Benutzereinstellungen
|
# Route für Benutzereinstellungen
|
||||||
@app.route('/settings', methods=['GET', 'POST'])
|
@app.route('/settings', methods=['GET', 'POST'])
|
||||||
@@ -456,32 +481,94 @@ def settings():
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
action = request.form.get('action')
|
action = request.form.get('action')
|
||||||
|
|
||||||
|
# Bestimme, ob es eine AJAX-Anfrage ist
|
||||||
|
is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.content_type and 'multipart/form-data' in request.content_type
|
||||||
|
|
||||||
if action == 'update_profile':
|
if action == 'update_profile':
|
||||||
current_user.bio = request.form.get('bio')
|
try:
|
||||||
|
current_user.bio = request.form.get('bio', '')
|
||||||
# Update avatar if provided
|
current_user.location = request.form.get('location', '')
|
||||||
avatar_url = request.form.get('avatar_url')
|
current_user.website = request.form.get('website', '')
|
||||||
if avatar_url:
|
|
||||||
current_user.avatar = avatar_url
|
|
||||||
|
|
||||||
db.session.commit()
|
# Update avatar if provided
|
||||||
flash('Profil erfolgreich aktualisiert!', 'success')
|
avatar_url = request.form.get('avatar_url')
|
||||||
|
if avatar_url:
|
||||||
|
current_user.avatar = avatar_url
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if is_ajax:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Profil erfolgreich aktualisiert!'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
flash('Profil erfolgreich aktualisiert!', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
app.logger.error(f"Fehler beim Aktualisieren des Profils: {str(e)}")
|
||||||
|
|
||||||
|
if is_ajax:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Fehler beim Aktualisieren des Profils'
|
||||||
|
}), 500
|
||||||
|
else:
|
||||||
|
flash('Fehler beim Aktualisieren des Profils', 'error')
|
||||||
|
|
||||||
elif action == 'update_password':
|
elif action == 'update_password':
|
||||||
current_password = request.form.get('current_password')
|
try:
|
||||||
new_password = request.form.get('new_password')
|
current_password = request.form.get('current_password')
|
||||||
confirm_password = request.form.get('confirm_password')
|
new_password = request.form.get('new_password')
|
||||||
|
confirm_password = request.form.get('confirm_password')
|
||||||
if not current_user.check_password(current_password):
|
|
||||||
flash('Aktuelles Passwort ist nicht korrekt', 'error')
|
if not current_user.check_password(current_password):
|
||||||
elif new_password != confirm_password:
|
if is_ajax:
|
||||||
flash('Neue Passwörter stimmen nicht überein', 'error')
|
return jsonify({
|
||||||
else:
|
'success': False,
|
||||||
current_user.set_password(new_password)
|
'message': 'Aktuelles Passwort ist nicht korrekt'
|
||||||
db.session.commit()
|
}), 400
|
||||||
flash('Passwort erfolgreich aktualisiert!', 'success')
|
else:
|
||||||
|
flash('Aktuelles Passwort ist nicht korrekt', 'error')
|
||||||
|
elif new_password != confirm_password:
|
||||||
|
if is_ajax:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Neue Passwörter stimmen nicht überein'
|
||||||
|
}), 400
|
||||||
|
else:
|
||||||
|
flash('Neue Passwörter stimmen nicht überein', 'error')
|
||||||
|
else:
|
||||||
|
current_user.set_password(new_password)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if is_ajax:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Passwort erfolgreich aktualisiert!'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
flash('Passwort erfolgreich aktualisiert!', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
app.logger.error(f"Fehler beim Aktualisieren des Passworts: {str(e)}")
|
||||||
|
|
||||||
|
if is_ajax:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Fehler beim Aktualisieren des Passworts'
|
||||||
|
}), 500
|
||||||
|
else:
|
||||||
|
flash('Fehler beim Aktualisieren des Passworts', 'error')
|
||||||
|
|
||||||
return redirect(url_for('settings'))
|
if not is_ajax:
|
||||||
|
return redirect(url_for('settings'))
|
||||||
|
else:
|
||||||
|
# Standardantwort für AJAX, falls keine spezifische Antwort zurückgegeben wurde
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Einstellungen aktualisiert'
|
||||||
|
})
|
||||||
|
|
||||||
return render_template('settings.html')
|
return render_template('settings.html')
|
||||||
|
|
||||||
@@ -598,16 +685,156 @@ def delete_mindmap(mindmap_id):
|
|||||||
# API-Endpunkte für Mindmap-Daten
|
# API-Endpunkte für Mindmap-Daten
|
||||||
@app.route('/api/mindmap/public')
|
@app.route('/api/mindmap/public')
|
||||||
def get_public_mindmap():
|
def get_public_mindmap():
|
||||||
"""Liefert die öffentliche Mindmap-Struktur."""
|
"""Liefert die Standard-Mindmap-Struktur basierend auf Kategorien."""
|
||||||
# Hole alle Kategorien der obersten Ebene
|
try:
|
||||||
root_categories = Category.query.filter_by(parent_id=None).all()
|
# Hole alle Hauptkategorien
|
||||||
|
categories = Category.query.filter_by(parent_id=None).all()
|
||||||
# Baue Baumstruktur auf
|
|
||||||
result = []
|
# Transformiere zu einer Baumstruktur
|
||||||
for category in root_categories:
|
category_tree = [build_category_tree(cat) for cat in categories]
|
||||||
result.append(build_category_tree(category))
|
|
||||||
|
return jsonify(category_tree)
|
||||||
return jsonify(result)
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Abrufen der Mindmap: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Mindmap konnte nicht geladen werden'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/mindmap/public/add_node', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def add_node_to_public_mindmap():
|
||||||
|
"""Fügt einen neuen Knoten zur öffentlichen Mindmap hinzu."""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
name = data.get('name')
|
||||||
|
description = data.get('description', '')
|
||||||
|
color_code = data.get('color_code', '#8b5cf6')
|
||||||
|
x_position = data.get('x_position', 0)
|
||||||
|
y_position = data.get('y_position', 0)
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Knotenname ist erforderlich'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Neuen Knoten erstellen
|
||||||
|
new_node = MindMapNode(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
color_code=color_code
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(new_node)
|
||||||
|
db.session.flush() # ID generieren
|
||||||
|
|
||||||
|
# Als Beitrag des aktuellen Benutzers markieren
|
||||||
|
new_node.contributed_by = current_user.id
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'node_id': new_node.id,
|
||||||
|
'message': 'Knoten erfolgreich hinzugefügt'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Fehler beim Hinzufügen des Knotens: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Fehler beim Hinzufügen des Knotens: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/mindmap/public/update_node/<int:node_id>', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_public_node(node_id):
|
||||||
|
"""Aktualisiert einen Knoten in der öffentlichen Mindmap."""
|
||||||
|
try:
|
||||||
|
node = MindMapNode.query.get_or_404(node_id)
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
# Aktualisiere Knotendaten
|
||||||
|
if 'name' in data:
|
||||||
|
node.name = data['name']
|
||||||
|
if 'description' in data:
|
||||||
|
node.description = data['description']
|
||||||
|
if 'color_code' in data:
|
||||||
|
node.color_code = data['color_code']
|
||||||
|
|
||||||
|
# Als bearbeitet markieren
|
||||||
|
node.last_modified = datetime.now(timezone.utc)
|
||||||
|
node.last_modified_by = current_user.id
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Knoten erfolgreich aktualisiert'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Fehler beim Aktualisieren des Knotens: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Fehler beim Aktualisieren des Knotens: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/mindmap/public/remove_node/<int:node_id>', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def remove_node_from_public_mindmap(node_id):
|
||||||
|
"""Entfernt einen Knoten aus der öffentlichen Mindmap."""
|
||||||
|
try:
|
||||||
|
node = MindMapNode.query.get_or_404(node_id)
|
||||||
|
|
||||||
|
# Lösche den Knoten
|
||||||
|
db.session.delete(node)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Knoten erfolgreich entfernt'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Fehler beim Entfernen des Knotens: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Fehler beim Entfernen des Knotens: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/mindmap/public/update_layout', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_public_layout():
|
||||||
|
"""Aktualisiert die Positionen der Knoten in der öffentlichen Mindmap."""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
positions = data.get('positions', [])
|
||||||
|
|
||||||
|
for pos in positions:
|
||||||
|
node_id = pos.get('node_id')
|
||||||
|
node = MindMapNode.query.get(node_id)
|
||||||
|
|
||||||
|
if node:
|
||||||
|
# Position aktualisieren
|
||||||
|
node.x_position = pos.get('x_position', 0)
|
||||||
|
node.y_position = pos.get('y_position', 0)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Layout erfolgreich aktualisiert'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Fehler beim Aktualisieren des Layouts: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Fehler beim Aktualisieren des Layouts: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
def build_category_tree(category):
|
def build_category_tree(category):
|
||||||
"""
|
"""
|
||||||
@@ -887,7 +1114,7 @@ def update_note(note_id):
|
|||||||
if color_code:
|
if color_code:
|
||||||
note.color_code = color_code
|
note.color_code = color_code
|
||||||
|
|
||||||
note.last_modified = datetime.utcnow()
|
note.last_modified = datetime.now(timezone.utc)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -1106,7 +1333,7 @@ def update_thought(thought_id):
|
|||||||
if 'color_code' in data:
|
if 'color_code' in data:
|
||||||
thought.color_code = data['color_code']
|
thought.color_code = data['color_code']
|
||||||
|
|
||||||
thought.last_modified = datetime.utcnow()
|
thought.last_modified = datetime.now(timezone.utc)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -1311,8 +1538,11 @@ def chat_with_assistant():
|
|||||||
})
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# OpenAI-Client mit dem API-Key initialisieren
|
||||||
|
client = OpenAI(api_key=api_key)
|
||||||
|
|
||||||
# Überprüfen ob OpenAI API-Key konfiguriert ist
|
# Überprüfen ob OpenAI API-Key konfiguriert ist
|
||||||
if not client.api_key or client.api_key.startswith("sk-dummy"):
|
if not api_key or api_key.startswith("sk-dummy"):
|
||||||
print("Warnung: OpenAI API-Key ist nicht oder nur als Dummy konfiguriert!")
|
print("Warnung: OpenAI API-Key ist nicht oder nur als Dummy konfiguriert!")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'error': 'Der OpenAI API-Key ist nicht korrekt konfiguriert. Bitte konfigurieren Sie die Umgebungsvariable OPENAI_API_KEY.'
|
'error': 'Der OpenAI API-Key ist nicht korrekt konfiguriert. Bitte konfigurieren Sie die Umgebungsvariable OPENAI_API_KEY.'
|
||||||
@@ -1322,12 +1552,13 @@ def chat_with_assistant():
|
|||||||
import time
|
import time
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Erhöhtes Timeout für die API-Anfrage
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model="gpt-4o-mini",
|
model="gpt-4o-mini",
|
||||||
messages=api_messages,
|
messages=api_messages,
|
||||||
max_tokens=1000, # Erhöht für ausführlichere Antworten und detaillierte Führungen
|
max_tokens=1000,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
timeout=20 # 20 Sekunden Timeout
|
timeout=60 # Erhöht auf 60 Sekunden für bessere Zuverlässigkeit
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"OpenAI API-Antwortzeit: {time.time() - start_time:.2f} Sekunden")
|
print(f"OpenAI API-Antwortzeit: {time.time() - start_time:.2f} Sekunden")
|
||||||
@@ -1341,12 +1572,30 @@ def chat_with_assistant():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
print(f"Fehler bei der OpenAI-Anfrage: {str(e)}")
|
error_message = str(e)
|
||||||
print(traceback.format_exc())
|
stack_trace = traceback.format_exc()
|
||||||
|
|
||||||
return jsonify({
|
print(f"Fehler bei der OpenAI-Anfrage: {error_message}")
|
||||||
'error': f'Fehler bei der OpenAI-Anfrage: {str(e)}'
|
print(f"Stack Trace: {stack_trace}")
|
||||||
}), 500
|
|
||||||
|
# Überprüfen auf spezifische Fehlertypen
|
||||||
|
if "timeout" in error_message.lower():
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es später erneut.'
|
||||||
|
}), 504
|
||||||
|
elif "rate limit" in error_message.lower():
|
||||||
|
return jsonify({
|
||||||
|
'error': 'API-Ratelimit erreicht. Bitte warten Sie einen Moment und versuchen Sie es erneut.'
|
||||||
|
}), 429
|
||||||
|
elif "internal server error" in error_message.lower() or "500" in error_message:
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Es ist ein Serverfehler aufgetreten. Unser Team wurde benachrichtigt.'
|
||||||
|
}), 500
|
||||||
|
else:
|
||||||
|
# Allgemeine Fehlermeldung
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.'
|
||||||
|
}), 500
|
||||||
|
|
||||||
def check_database_query(user_message):
|
def check_database_query(user_message):
|
||||||
"""
|
"""
|
||||||
@@ -1479,14 +1728,42 @@ def refresh_mindmap():
|
|||||||
if Category.query.count() == 0:
|
if Category.query.count() == 0:
|
||||||
create_default_categories()
|
create_default_categories()
|
||||||
|
|
||||||
|
# Überprüfe, ob wir bereits einen "Wissen"-Knoten haben
|
||||||
|
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
|
||||||
|
|
||||||
|
# Wenn kein "Wissen"-Knoten existiert, erstelle ihn
|
||||||
|
if not wissen_node:
|
||||||
|
wissen_node = MindMapNode(
|
||||||
|
name="Wissen",
|
||||||
|
description="Zentrale Wissensbasis",
|
||||||
|
color_code="#4299E1",
|
||||||
|
is_public=True
|
||||||
|
)
|
||||||
|
db.session.add(wissen_node)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
# Hole alle Kategorien und Knoten
|
# Hole alle Kategorien und Knoten
|
||||||
categories = Category.query.filter_by(parent_id=None).all()
|
categories = Category.query.filter_by(parent_id=None).all()
|
||||||
category_tree = [build_category_tree(cat) for cat in categories]
|
category_tree = [build_category_tree(cat) for cat in categories]
|
||||||
|
|
||||||
# Hole alle Mindmap-Knoten
|
# Hole alle Mindmap-Knoten außer dem "Wissen"-Knoten
|
||||||
nodes = MindMapNode.query.all()
|
nodes = MindMapNode.query.filter(MindMapNode.id != wissen_node.id).all()
|
||||||
node_data = []
|
|
||||||
|
|
||||||
|
# Vorbereiten der Node- und Edge-Arrays für die Antwort
|
||||||
|
node_data = []
|
||||||
|
edge_data = []
|
||||||
|
|
||||||
|
# Zuerst den "Wissen"-Knoten hinzufügen
|
||||||
|
node_data.append({
|
||||||
|
'id': wissen_node.id,
|
||||||
|
'name': wissen_node.name,
|
||||||
|
'description': wissen_node.description or '',
|
||||||
|
'color_code': wissen_node.color_code or '#4299E1',
|
||||||
|
'thought_count': len(wissen_node.thoughts),
|
||||||
|
'category_id': wissen_node.category_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Dann die anderen Knoten
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
node_obj = {
|
node_obj = {
|
||||||
'id': node.id,
|
'id': node.id,
|
||||||
@@ -1497,15 +1774,28 @@ def refresh_mindmap():
|
|||||||
'category_id': node.category_id
|
'category_id': node.category_id
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verbindungen hinzufügen
|
# Verbinde alle Top-Level-Knoten mit dem Wissen-Knoten
|
||||||
node_obj['connections'] = [{'target': child.id} for child in node.children]
|
if not node.parents.all():
|
||||||
|
edge_data.append({
|
||||||
|
'source': wissen_node.id,
|
||||||
|
'target': node.id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Verbindungen zwischen vorhandenen Knoten hinzufügen
|
||||||
|
node_children = node.children.all()
|
||||||
|
for child in node_children:
|
||||||
|
edge_data.append({
|
||||||
|
'source': node.id,
|
||||||
|
'target': child.id
|
||||||
|
})
|
||||||
|
|
||||||
node_data.append(node_obj)
|
node_data.append(node_obj)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'categories': category_tree,
|
'categories': category_tree,
|
||||||
'nodes': node_data
|
'nodes': node_data,
|
||||||
|
'edges': edge_data
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1515,21 +1805,46 @@ def refresh_mindmap():
|
|||||||
'error': 'Datenbankverbindung konnte nicht hergestellt werden'
|
'error': 'Datenbankverbindung konnte nicht hergestellt werden'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
# Route zur Mindmap HTML-Seite
|
# Route zur Mindmap HTML-Seite
|
||||||
@app.route('/mindmap')
|
@app.route('/mindmap')
|
||||||
def mindmap_page():
|
def mindmap_page():
|
||||||
return render_template('mindmap.html')
|
return render_template('mindmap.html')
|
||||||
|
|
||||||
# Fehlerbehandlung
|
# Weiterleitung für Community/Forum-Routen
|
||||||
@app.errorhandler(404)
|
@app.route('/community')
|
||||||
def not_found(e):
|
@app.route('/Community')
|
||||||
return jsonify({'error': 'Nicht gefunden'}), 404
|
@app.route('/forum')
|
||||||
|
@app.route('/Forum')
|
||||||
|
@app.route('/community_forum')
|
||||||
|
def redirect_community():
|
||||||
|
"""Leitet alle Community/Forum-URLs zur Startseite um"""
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
@app.errorhandler(400)
|
@app.route('/static/js/mindmap-init.js')
|
||||||
def bad_request(e):
|
def serve_mindmap_init_js():
|
||||||
return jsonify({'error': 'Fehlerhafte Anfrage'}), 400
|
"""Bedient die Mindmap-Initialisierungsdatei."""
|
||||||
|
return app.send_static_file('js/mindmap-init.js'), 200, {'Content-Type': 'application/javascript'}
|
||||||
|
|
||||||
@app.errorhandler(500)
|
# Datenbank-Update-Route (admin-geschützt)
|
||||||
def server_error(e):
|
@app.route('/admin/update-database', methods=['GET', 'POST'])
|
||||||
return jsonify({'error': 'Serverfehler'}), 500
|
@admin_required
|
||||||
|
def admin_update_database():
|
||||||
|
"""Admin-Route zum Aktualisieren der Datenbank"""
|
||||||
|
message = None
|
||||||
|
success = None
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
import update_db
|
||||||
|
update_success = update_db.update_user_table()
|
||||||
|
if update_success:
|
||||||
|
message = "Die Datenbank wurde erfolgreich aktualisiert."
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
message = "Es gab ein Problem bei der Aktualisierung der Datenbank."
|
||||||
|
success = False
|
||||||
|
except Exception as e:
|
||||||
|
message = f"Fehler: {str(e)}"
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return render_template('admin/update_database.html', message=message, success=success)
|
||||||
BIN
backup/archiv_0.1.zip
Normal file
BIN
backup/archiv_0.1.zip
Normal file
Binary file not shown.
Binary file not shown.
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
image: systades_app:latest
|
||||||
|
container_name: systades_app
|
||||||
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- ./database:/app/database
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
# Kopiere diese Datei zu .env und passe die Werte an
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=your-secret-key-replace-in-production
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database/systades.db'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
|
|||||||
BIN
migrations/__pycache__/env.cpython-313.pyc
Normal file
BIN
migrations/__pycache__/env.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
40
migrations/versions/add_missing_user_fields.py
Normal file
40
migrations/versions/add_missing_user_fields.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Add missing user fields
|
||||||
|
|
||||||
|
Revision ID: 5a23f8c6db37
|
||||||
|
Revises: d4406f5b12f7
|
||||||
|
Create Date: 2025-05-02 10:45:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '5a23f8c6db37'
|
||||||
|
down_revision = 'd4406f5b12f7'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('bio', sa.Text(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('location', sa.String(length=100), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('website', sa.String(length=200), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('avatar', sa.String(length=200), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('last_login', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('last_login')
|
||||||
|
batch_op.drop_column('avatar')
|
||||||
|
batch_op.drop_column('website')
|
||||||
|
batch_op.drop_column('location')
|
||||||
|
batch_op.drop_column('bio')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
56
models.py
56
models.py
@@ -53,11 +53,20 @@ class User(db.Model, UserMixin):
|
|||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
is_active = db.Column(db.Boolean, default=True)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
||||||
|
bio = db.Column(db.Text, nullable=True) # Profil-Bio
|
||||||
|
location = db.Column(db.String(100), nullable=True) # Standort
|
||||||
|
website = db.Column(db.String(200), nullable=True) # Website
|
||||||
|
avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL
|
||||||
|
last_login = db.Column(db.DateTime, nullable=True) # Letzter Login
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
threads = db.relationship('Thread', backref='creator', lazy=True)
|
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||||
messages = db.relationship('Message', backref='author', lazy=True)
|
messages = db.relationship('Message', backref='author', lazy=True)
|
||||||
projects = db.relationship('Project', backref='owner', lazy=True)
|
projects = db.relationship('Project', backref='owner', lazy=True)
|
||||||
|
mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
||||||
|
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
||||||
|
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
||||||
|
lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic'))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<User {self.username}>'
|
return f'<User {self.username}>'
|
||||||
@@ -67,6 +76,14 @@ class User(db.Model, UserMixin):
|
|||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password, password)
|
return check_password_hash(self.password, password)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_admin(self):
|
||||||
|
return self.role == 'admin'
|
||||||
|
|
||||||
|
@is_admin.setter
|
||||||
|
def is_admin(self, value):
|
||||||
|
self.role = 'admin' if value else 'user'
|
||||||
|
|
||||||
class Category(db.Model):
|
class Category(db.Model):
|
||||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||||
@@ -305,4 +322,41 @@ class Document(db.Model):
|
|||||||
file_size = db.Column(db.Integer, nullable=True)
|
file_size = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Document {self.title}>'
|
return f'<Document {self.title}>'
|
||||||
|
|
||||||
|
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
|
||||||
|
class ForumCategory(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
node = db.relationship('MindMapNode', backref='forum_category')
|
||||||
|
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ForumCategory {self.title}>'
|
||||||
|
|
||||||
|
# Forum-Beitrag-Modell
|
||||||
|
class ForumPost(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False)
|
||||||
|
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
|
||||||
|
is_pinned = db.Column(db.Boolean, default=False)
|
||||||
|
is_locked = db.Column(db.Boolean, default=False)
|
||||||
|
view_count = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
author = db.relationship('User', backref='forum_posts')
|
||||||
|
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ForumPost {self.title}>'
|
||||||
53
start.sh
Normal file
53
start.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env powershell
|
||||||
|
# Windows PowerShell-Version des Start-Skripts
|
||||||
|
# Datum: 01.05.2025
|
||||||
|
|
||||||
|
# Docker-Status prüfen
|
||||||
|
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
|
||||||
|
try {
|
||||||
|
$status = docker ps -q
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
|
||||||
|
Write-Host $_.Exception.Message
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alte Container stoppen und entfernen
|
||||||
|
$containerExists = docker ps -a --filter "name=systades_app" -q
|
||||||
|
if ($containerExists) {
|
||||||
|
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
|
||||||
|
docker rm -f systades_app
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alte Images löschen
|
||||||
|
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
|
||||||
|
docker rmi -f systades_app:latest
|
||||||
|
|
||||||
|
# Stelle sicher, dass das Datenbankverzeichnis existiert
|
||||||
|
if (-not (Test-Path "database")) {
|
||||||
|
New-Item -Path "database" -ItemType Directory -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker-Compose Setup neu bauen
|
||||||
|
Write-Host "Baue Container neu..." -ForegroundColor Green
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
# Docker-Compose neu starten
|
||||||
|
Write-Host "Starte Container..." -ForegroundColor Green
|
||||||
|
docker-compose up -d --force-recreate
|
||||||
|
|
||||||
|
# Warte kurz und prüfe, ob der Container läuft
|
||||||
|
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
docker ps | Select-String "systades_app"
|
||||||
|
|
||||||
|
# Ausgabe
|
||||||
|
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
|
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
|
||||||
|
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
1
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
1
static/541AABD7-4E37-44ED-B491-1459C8C19699.PNG
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
/* ChatGPT Assistent Styles - Verbesserte Version */
|
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
|
bottom: 5.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 85vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-chat {
|
#assistant-chat {
|
||||||
@@ -10,6 +13,15 @@
|
|||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: calc(100vw - 2rem);
|
max-width: calc(100vw - 2rem);
|
||||||
|
max-height: 80vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-history {
|
||||||
|
max-height: calc(80vh - 150px);
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||||
|
padding-bottom: 2rem; /* Zusätzlicher Abstand unten */
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-toggle {
|
#assistant-toggle {
|
||||||
@@ -22,11 +34,6 @@
|
|||||||
transform: scale(1.1) rotate(10deg);
|
transform: scale(1.1) rotate(10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-history {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
#assistant-history::-webkit-scrollbar {
|
#assistant-history::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
@@ -142,14 +149,21 @@
|
|||||||
.typing-indicator span {
|
.typing-indicator span {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
background-color: #888;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
opacity: 0.4;
|
opacity: 0.6;
|
||||||
animation: bounce 1.4s infinite ease-in-out;
|
animation: bounce 1.4s infinite ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.dark .typing-indicator span {
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .typing-indicator span {
|
||||||
|
background-color: rgba(107, 114, 128, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
|
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
|
||||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||||
@@ -173,11 +187,12 @@
|
|||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
#assistant-chat {
|
#assistant-chat {
|
||||||
width: calc(100vw - 2rem) !important;
|
width: calc(100vw - 2rem) !important;
|
||||||
|
max-height: 65vh !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
bottom: 1rem;
|
bottom: 6rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,4 +215,38 @@ main {
|
|||||||
|
|
||||||
footer {
|
footer {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Farbkontraste für Nachrichtenblasen */
|
||||||
|
.user-message {
|
||||||
|
background-color: rgba(124, 58, 237, 0.1) !important;
|
||||||
|
color: #4B5563 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .user-message {
|
||||||
|
background-color: rgba(124, 58, 237, 0.2) !important;
|
||||||
|
color: #F9FAFB !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message {
|
||||||
|
background-color: #F3F4F6 !important;
|
||||||
|
color: #1F2937 !important;
|
||||||
|
border-left: 3px solid #8B5CF6;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .assistant-message {
|
||||||
|
background-color: rgba(31, 41, 55, 0.5) !important;
|
||||||
|
color: #F9FAFB !important;
|
||||||
|
border-left: 3px solid #8B5CF6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat-Assistent-Position im Footer-Bereich anpassen */
|
||||||
|
.chat-assistant {
|
||||||
|
max-height: 75vh;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant .chat-messages {
|
||||||
|
max-height: calc(75vh - 180px);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
--light-bg: #f9fafb;
|
--light-bg: #f9fafb;
|
||||||
--light-text: #1e293b;
|
--light-text: #1e293b;
|
||||||
--light-heading: #0f172a;
|
--light-heading: #0f172a;
|
||||||
--light-primary: #3b82f6;
|
--light-primary: #7c3aed;
|
||||||
--light-primary-hover: #4f46e5;
|
--light-primary-hover: #6d28d9;
|
||||||
--light-secondary: #6b7280;
|
--light-secondary: #6b7280;
|
||||||
--light-border: #e5e7eb;
|
--light-border: #e5e7eb;
|
||||||
--light-card-bg: rgba(255, 255, 255, 0.92);
|
--light-card-bg: rgba(255, 255, 255, 0.92);
|
||||||
@@ -68,18 +68,37 @@ body {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Mode */
|
/* Strikte Trennung: Dark Mode */
|
||||||
html.dark body {
|
html.dark body,
|
||||||
|
body.dark {
|
||||||
background-color: var(--bg-primary-dark);
|
background-color: var(--bg-primary-dark);
|
||||||
color: var(--text-primary-dark);
|
color: var(--text-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode */
|
/* Strikte Trennung: Light Mode */
|
||||||
|
html:not(.dark) body,
|
||||||
body:not(.dark) {
|
body:not(.dark) {
|
||||||
background-color: var(--light-bg);
|
background-color: var(--light-bg);
|
||||||
color: var(--light-text);
|
color: var(--light-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Trennung: Container und Karten */
|
||||||
|
body.dark .card,
|
||||||
|
body.dark .glass-card,
|
||||||
|
body.dark .panel {
|
||||||
|
background-color: var(--bg-secondary-dark);
|
||||||
|
border-color: var(--border-dark);
|
||||||
|
color: var(--text-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .card,
|
||||||
|
body:not(.dark) .glass-card,
|
||||||
|
body:not(.dark) .panel {
|
||||||
|
background-color: var(--light-card-bg);
|
||||||
|
border-color: var(--light-border);
|
||||||
|
color: var(--light-text);
|
||||||
|
}
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -388,7 +407,7 @@ html.dark ::-webkit-scrollbar-thumb:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
font-size: 1.5rem;
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,22 +474,60 @@ body:not(.dark) a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode Buttons */
|
/* Light Mode Buttons */
|
||||||
|
body:not(.dark) button:not(.toggle):not(.plain-btn) {
|
||||||
|
color: white !important;
|
||||||
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
body:not(.dark) .btn,
|
body:not(.dark) .btn,
|
||||||
body:not(.dark) button:not(.toggle) {
|
body:not(.dark) .btn-primary,
|
||||||
background-color: var(--light-primary);
|
body:not(.dark) .btn-secondary,
|
||||||
color: white;
|
body:not(.dark) .btn-success,
|
||||||
border: none;
|
body:not(.dark) .btn-danger,
|
||||||
box-shadow: var(--light-shadow);
|
body:not(.dark) .btn-warning,
|
||||||
border-radius: 0.375rem;
|
body:not(.dark) .btn-info {
|
||||||
padding: 0.5rem 1rem;
|
color: white !important;
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.dark) .btn:hover,
|
body:not(.dark) .btn:hover,
|
||||||
body:not(.dark) button:not(.toggle):hover {
|
body:not(.dark) button:not(.toggle):hover {
|
||||||
background-color: var(--light-primary-hover);
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark/Light Mode Switch Button */
|
||||||
|
.theme-toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(to right, #7c3aed, #3b82f6);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle.dark::after {
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover::after {
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode Cards und Panels */
|
/* Light Mode Cards und Panels */
|
||||||
@@ -523,4 +580,489 @@ body:not(.dark) .navbar {
|
|||||||
background-color: var(--light-navbar-bg);
|
background-color: var(--light-navbar-bg);
|
||||||
box-shadow: var(--light-shadow);
|
box-shadow: var(--light-shadow);
|
||||||
border-bottom: 1px solid var(--light-border);
|
border-bottom: 1px solid var(--light-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Erweiterte Light-Mode-spezifische Stile */
|
||||||
|
body:not(.dark) .glass-effect {
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(209, 213, 219, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .card {
|
||||||
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
|
border: 1px solid var(--light-border);
|
||||||
|
box-shadow: var(--light-shadow);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .card:hover {
|
||||||
|
box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Buttons mit verbesserter Lesbarkeit */
|
||||||
|
body:not(.dark) .btn-primary {
|
||||||
|
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
box-shadow: 0 2px 4px rgba(91, 33, 182, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-primary:hover {
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
|
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-secondary {
|
||||||
|
background: linear-gradient(135deg, #ffffff, #f9fafb);
|
||||||
|
color: #1f2937;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-secondary:hover {
|
||||||
|
background: linear-gradient(135deg, #f9fafb, #f3f4f6);
|
||||||
|
border-color: #d1d5db;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-outline {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--light-primary);
|
||||||
|
border: 1px solid var(--light-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-outline:hover {
|
||||||
|
background-color: rgba(124, 58, 237, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Formulare */
|
||||||
|
body:not(.dark) input,
|
||||||
|
body:not(.dark) select,
|
||||||
|
body:not(.dark) textarea {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) input:focus,
|
||||||
|
body:not(.dark) select:focus,
|
||||||
|
body:not(.dark) textarea:focus {
|
||||||
|
border-color: var(--light-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Navigation */
|
||||||
|
body:not(.dark) .sidebar {
|
||||||
|
background-color: white;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .sidebar-link {
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .sidebar-link:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: var(--light-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .sidebar-link.active {
|
||||||
|
background-color: rgba(124, 58, 237, 0.08);
|
||||||
|
color: var(--light-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Tabellen */
|
||||||
|
body:not(.dark) table {
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) th {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) tr:nth-child(even) {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) tr:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Icons */
|
||||||
|
body:not(.dark) .icon {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .icon-primary {
|
||||||
|
color: var(--light-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Alerts/Benachrichtigungen */
|
||||||
|
body:not(.dark) .alert-info {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
border-left: 4px solid #3b82f6;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .alert-success {
|
||||||
|
background-color: #ecfdf5;
|
||||||
|
border-left: 4px solid #10b981;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .alert-warning {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .alert-error {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border-left: 4px solid #ef4444;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Badge */
|
||||||
|
body:not(.dark) .badge {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .badge-primary {
|
||||||
|
background-color: rgba(124, 58, 237, 0.1);
|
||||||
|
color: var(--light-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Mindmap spezifisch */
|
||||||
|
body:not(.dark) #cy {
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .node {
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .node:hover,
|
||||||
|
body:not(.dark) .node.selected {
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.5), 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .edge {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .edge:hover,
|
||||||
|
body:not(.dark) .edge.selected {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer im Light Mode */
|
||||||
|
body:not(.dark) footer {
|
||||||
|
background-color: rgba(249, 250, 251, 0.7);
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alpine.js Transitions im Light Mode */
|
||||||
|
body:not(.dark) [x-cloak] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suchfeldstyling im Light Mode */
|
||||||
|
body:not(.dark) .search-container input {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .search-container input:focus {
|
||||||
|
border-color: var(--light-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .search-results {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .search-result-item:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile und Benutzermenü im Light Mode */
|
||||||
|
body:not(.dark) .avatar {
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .user-dropdown {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .user-dropdown-item:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medienabfragen für Responsivität */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
/* Optimierungen für Smartphones */
|
||||||
|
body {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-heading {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card, .panel, .glass-card {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimierte Touch-Ziele für mobile Geräte */
|
||||||
|
button, .btn, .nav-link, .menu-item {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Lesbarkeit auf kleinen Bildschirmen */
|
||||||
|
p, li, input, textarea, button, .text-sm {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anpassungen für Tabellen auf kleinen Bildschirmen */
|
||||||
|
table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimierte Formulare */
|
||||||
|
input, select, textarea {
|
||||||
|
font-size: 16px; /* Verhindert iOS-Zoom bei Fokus */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserter Abstand für Touch-Targets */
|
||||||
|
nav a, nav button, .menu-item {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) and (max-width: 1024px) {
|
||||||
|
/* Optimierungen für Tablets */
|
||||||
|
.container {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zweispaltige Layouts für mittlere Bildschirme */
|
||||||
|
.grid-cols-1 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimierte Navigationsleiste */
|
||||||
|
.navbar {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1025px) {
|
||||||
|
/* Optimierungen für Desktop */
|
||||||
|
.container {
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mehrspaltige Layouts für große Bildschirme */
|
||||||
|
.grid-cols-1 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover-Effekte nur auf Desktop-Geräten */
|
||||||
|
.card:hover, .panel:hover, .glass-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop-spezifische Animationen */
|
||||||
|
.animate-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-hover:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design improvements */
|
||||||
|
.responsive-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-flex {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-flex > * {
|
||||||
|
flex: 1 1 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility improvements */
|
||||||
|
.focus-visible:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-primary-light);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .focus-visible:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background: white !important;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav, footer, button, .no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
main, article, .card, .panel, .container {
|
||||||
|
width: 100% !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
color: black !important;
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: black !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a::after {
|
||||||
|
content: " (" attr(href) ")";
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
page-break-after: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page {
|
||||||
|
margin: 2cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode KI-Chatfenster */
|
||||||
|
body:not(.dark) .chat-container {
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .chat-message-ai {
|
||||||
|
background-color: rgba(124, 58, 237, 0.1);
|
||||||
|
border: 1px solid rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .chat-message-user {
|
||||||
|
background-color: rgba(59, 130, 246, 0.1);
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anpassung der Chatfenster-Größe */
|
||||||
|
.chat-assistant {
|
||||||
|
max-height: 85vh; /* Vergrößert von 80vh */
|
||||||
|
bottom: 1rem; /* Etwas höher positionieren */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant .chat-messages {
|
||||||
|
max-height: calc(85vh - 180px); /* Angepasst für größeres Fenster */
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 2rem; /* Zusätzlicher Abstand um Abschneiden zu vermeiden */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserungen für das Mobilmenü */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-menu-container {
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatgpt-assistant {
|
||||||
|
bottom: 4.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant {
|
||||||
|
max-height: 70vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant .chat-messages {
|
||||||
|
max-height: calc(70vh - 160px) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
253
static/css/mindmap.css
Normal file
253
static/css/mindmap.css
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/* Mindmap Container Styles */
|
||||||
|
.mindmap-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 600px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar Styles */
|
||||||
|
.mindmap-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mindmap-toolbar {
|
||||||
|
background: rgba(30, 41, 59, 0.8);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar Buttons */
|
||||||
|
.mindmap-toolbar button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-toolbar button:hover {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-toolbar button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-toolbar button i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Export Group Styles */
|
||||||
|
.export-group {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .export-options {
|
||||||
|
background: rgba(30, 41, 59, 0.9);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-group:hover .export-options {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options button {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 8px 12px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Context Menu Styles */
|
||||||
|
.mindmap-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 8px;
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mindmap-context-menu {
|
||||||
|
background: rgba(30, 41, 59, 0.9);
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-context-menu button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-context-menu button:hover {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-context-menu button i {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node Styles */
|
||||||
|
.mindmap-node {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border: 2px solid var(--accent-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node:hover {
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-primary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node.selected {
|
||||||
|
border-color: var(--accent-secondary);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge Styles */
|
||||||
|
.mindmap-edge {
|
||||||
|
width: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mindmap-edge {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-edge:hover {
|
||||||
|
width: 3px;
|
||||||
|
background-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation Styles */
|
||||||
|
@keyframes nodeAppear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node-new {
|
||||||
|
animation: nodeAppear 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mindmap-toolbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options {
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.mindmap-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid var(--bg-secondary);
|
||||||
|
border-top-color: var(--accent-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip Styles */
|
||||||
|
.mindmap-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 200px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mindmap-tooltip {
|
||||||
|
background: rgba(30, 41, 59, 0.9);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
11
static/img/default-avatar.svg
Normal file
11
static/img/default-avatar.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 583 B |
@@ -1,25 +1,54 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
|
Generate favicon.ico from SVG using cairosvg and PIL
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
from cairosvg import svg2png
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import cairosvg
|
|
||||||
|
|
||||||
# Pfad zum SVG-Favicon
|
# Verzeichnis dieses Skripts
|
||||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
# Ausgabepfad für das PNG
|
|
||||||
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
|
|
||||||
# Ausgabepfad für das ICO
|
|
||||||
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
|
|
||||||
|
|
||||||
# SVG zu PNG konvertieren
|
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
|
||||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
"""Convert SVG to multi-size ICO file"""
|
||||||
|
img_io = io.BytesIO()
|
||||||
|
|
||||||
|
# Höchste Auflösung für Zwischenspeicherung
|
||||||
|
max_size = max(sizes)
|
||||||
|
|
||||||
|
# SVG in PNG konvertieren
|
||||||
|
with open(svg_path, 'rb') as svg_file:
|
||||||
|
svg_data = svg_file.read()
|
||||||
|
svg2png(bytestring=svg_data, write_to=img_io, output_width=max_size, output_height=max_size)
|
||||||
|
|
||||||
|
# PNG in verschiedene Größen konvertieren
|
||||||
|
img = Image.open(img_io)
|
||||||
|
|
||||||
|
# Alle Größen für das ICO-Format vorbereiten
|
||||||
|
img_list = []
|
||||||
|
for size in sizes:
|
||||||
|
resized_img = img.resize((size, size), Image.LANCZOS)
|
||||||
|
img_list.append(resized_img)
|
||||||
|
|
||||||
|
# ICO-Datei speichern
|
||||||
|
img_list[0].save(
|
||||||
|
ico_path,
|
||||||
|
format='ICO',
|
||||||
|
sizes=[(img.width, img.height) for img in img_list],
|
||||||
|
append_images=img_list[1:]
|
||||||
|
)
|
||||||
|
print(f"Favicon {ico_path} wurde erstellt!")
|
||||||
|
|
||||||
# PNG zu ICO konvertieren
|
# Ursprüngliches Favicon konvertieren
|
||||||
img = Image.open(png_path)
|
svg_to_ico(
|
||||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
os.path.join(CURRENT_DIR, 'favicon.svg'),
|
||||||
|
os.path.join(CURRENT_DIR, 'favicon.ico')
|
||||||
|
)
|
||||||
|
|
||||||
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
# Neues Neuron-Favicon konvertieren
|
||||||
|
svg_to_ico(
|
||||||
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
|
||||||
# os.remove(png_path)
|
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
|
||||||
|
)
|
||||||
29
static/img/neuron-favicon.svg
Normal file
29
static/img/neuron-favicon.svg
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Hintergrund -->
|
||||||
|
<rect width="32" height="32" rx="8" fill="#6d28d9" />
|
||||||
|
|
||||||
|
<!-- Mindmap-Punkte -->
|
||||||
|
<!-- Zentraler Punkt -->
|
||||||
|
<circle cx="16" cy="16" r="3.5" fill="#a78bfa" />
|
||||||
|
|
||||||
|
<!-- Umgebende Punkte -->
|
||||||
|
<circle cx="8" cy="10" r="2.5" fill="#8b5cf6" />
|
||||||
|
<circle cx="24" cy="10" r="2.5" fill="#8b5cf6" />
|
||||||
|
<circle cx="16" cy="26" r="2.5" fill="#8b5cf6" />
|
||||||
|
|
||||||
|
<!-- Verbindende Linien -->
|
||||||
|
<path d="M16 16 L8 10" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||||
|
<path d="M16 16 L24 10" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||||
|
<path d="M16 16 L16 26" stroke="white" stroke-width="1" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Weitere Verbindungslinien für mehr Komplexität -->
|
||||||
|
<path d="M8 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||||
|
<path d="M24 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||||
|
<path d="M8 10 L24 10" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
|
||||||
|
|
||||||
|
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
|
||||||
|
<circle cx="5" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="27" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="20" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="12" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
59
static/img/neuron-logo.svg
Normal file
59
static/img/neuron-logo.svg
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Hintergrund mit Farbverlauf -->
|
||||||
|
<rect width="64" height="64" rx="16" fill="url(#paint0_linear)" />
|
||||||
|
|
||||||
|
<!-- Mindmap-Punkte -->
|
||||||
|
<!-- Zentraler Punkt -->
|
||||||
|
<circle cx="32" cy="32" r="8" fill="url(#glow_gradient)" filter="url(#glow)" />
|
||||||
|
|
||||||
|
<!-- Umgebende Punkte -->
|
||||||
|
<circle cx="16" cy="20" r="6" fill="#8b5cf6" />
|
||||||
|
<circle cx="48" cy="20" r="6" fill="#8b5cf6" />
|
||||||
|
<circle cx="32" cy="52" r="6" fill="#8b5cf6" />
|
||||||
|
<circle cx="16" cy="48" r="4" fill="#a78bfa" />
|
||||||
|
<circle cx="48" cy="48" r="4" fill="#a78bfa" />
|
||||||
|
|
||||||
|
<!-- Verbindende Linien (Hauptpfade) -->
|
||||||
|
<path d="M32 32 L16 20" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M32 32 L48 20" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M32 32 L32 52" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M32 32 L16 48" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
<path d="M32 32 L48 48" stroke="white" stroke-width="2" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Zusätzliche Verbindungslinien -->
|
||||||
|
<path d="M16 20 L16 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
<path d="M48 20 L48 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
<path d="M16 20 L48 20" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
<path d="M16 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
<path d="M48 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
|
||||||
|
|
||||||
|
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
|
||||||
|
<circle cx="10" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="54" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="40" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="24" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
|
||||||
|
<circle cx="20" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||||
|
<circle cx="44" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||||
|
<circle cx="32" cy="16" r="1.2" fill="#ddd6fe" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Definitionen für Farbverläufe und Effekte -->
|
||||||
|
<defs>
|
||||||
|
<!-- Haupthintergrund-Farbverlauf -->
|
||||||
|
<linearGradient id="paint0_linear" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#6d28d9" />
|
||||||
|
<stop offset="1" stop-color="#4c1d95" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Glüheffekt für den zentralen Punkt -->
|
||||||
|
<filter id="glow" x="20" y="20" width="24" height="24" filterUnits="userSpaceOnUse">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- Farbverlauf für den zentralen Punkt -->
|
||||||
|
<linearGradient id="glow_gradient" x1="24" y1="24" x2="40" y2="40" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#a78bfa" />
|
||||||
|
<stop offset="1" stop-color="#8b5cf6" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
1249
static/js/mindmap-init.js
Normal file
1249
static/js/mindmap-init.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -247,130 +247,63 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = sender === 'user'
|
bubble.className = sender === 'user'
|
||||||
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
|
||||||
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||||
|
|
||||||
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
|
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||||
let formattedText = '';
|
if (this.markdownParser) {
|
||||||
|
bubble.innerHTML = this.markdownParser.parse(text);
|
||||||
if (sender === 'assistant' && this.markdownParser) {
|
|
||||||
// Für Assistentnachrichten Markdown verwenden
|
|
||||||
try {
|
|
||||||
formattedText = this.markdownParser.parse(text);
|
|
||||||
|
|
||||||
// CSS für Markdown-Formatierung hinzufügen
|
|
||||||
const markdownStyles = `
|
|
||||||
.markdown-bubble h1, .markdown-bubble h2, .markdown-bubble h3,
|
|
||||||
.markdown-bubble h4, .markdown-bubble h5, .markdown-bubble h6 {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
.markdown-bubble h1 { font-size: 1.4rem; }
|
|
||||||
.markdown-bubble h2 { font-size: 1.3rem; }
|
|
||||||
.markdown-bubble h3 { font-size: 1.2rem; }
|
|
||||||
.markdown-bubble h4 { font-size: 1.1rem; }
|
|
||||||
.markdown-bubble ul, .markdown-bubble ol {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
.markdown-bubble ul { list-style-type: disc; }
|
|
||||||
.markdown-bubble ol { list-style-type: decimal; }
|
|
||||||
.markdown-bubble p { margin: 0.5rem 0; }
|
|
||||||
.markdown-bubble code {
|
|
||||||
font-family: monospace;
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 1px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
.markdown-bubble pre {
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
.markdown-bubble pre code {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.markdown-bubble blockquote {
|
|
||||||
border-left: 3px solid rgba(0, 0, 0, 0.2);
|
|
||||||
padding-left: 0.8rem;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.dark .markdown-bubble code {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
.dark .markdown-bubble pre {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
.dark .markdown-bubble blockquote {
|
|
||||||
border-left-color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Füge die Styles hinzu, wenn sie noch nicht vorhanden sind
|
|
||||||
if (!document.querySelector('#markdown-chat-styles')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'markdown-chat-styles';
|
|
||||||
style.textContent = markdownStyles;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Klasse für Markdown-Formatierung hinzufügen
|
|
||||||
bubble.classList.add('markdown-bubble');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler bei der Markdown-Formatierung:', error);
|
|
||||||
// Fallback zur einfachen Formatierung
|
|
||||||
formattedText = text.split('\n').map(line => {
|
|
||||||
if (line.trim() === '') return '<br>';
|
|
||||||
return `<p>${line}</p>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Für Benutzernachrichten einfache Formatierung
|
bubble.textContent = text;
|
||||||
formattedText = text.split('\n').map(line => {
|
|
||||||
if (line.trim() === '') return '<br>';
|
|
||||||
return `<p>${line}</p>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bubble.innerHTML = formattedText;
|
// Links in der Nachricht klickbar machen
|
||||||
|
const links = bubble.querySelectorAll('a');
|
||||||
|
links.forEach(link => {
|
||||||
|
link.target = '_blank';
|
||||||
|
link.rel = 'noopener noreferrer';
|
||||||
|
link.className = 'text-primary-600 dark:text-primary-400 underline';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Code-Blöcke stylen
|
||||||
|
const codeBlocks = bubble.querySelectorAll('pre');
|
||||||
|
codeBlocks.forEach(block => {
|
||||||
|
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
|
||||||
|
});
|
||||||
|
|
||||||
|
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
|
||||||
|
inlineCode.forEach(code => {
|
||||||
|
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
|
||||||
|
});
|
||||||
|
|
||||||
messageEl.appendChild(bubble);
|
messageEl.appendChild(bubble);
|
||||||
|
this.chatHistory.appendChild(messageEl);
|
||||||
|
|
||||||
if (this.chatHistory) {
|
// Scrolle zum Ende des Chat-Verlaufs
|
||||||
this.chatHistory.appendChild(messageEl);
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt Vorschläge als klickbare Pills an
|
* Zeigt Vorschläge für mögliche Fragen an
|
||||||
* @param {string[]} suggestions - Liste von Vorschlägen
|
* @param {Array} suggestions - Array von Vorschlägen
|
||||||
*/
|
*/
|
||||||
showSuggestions(suggestions) {
|
showSuggestions(suggestions) {
|
||||||
if (!this.suggestionArea) return;
|
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
|
||||||
|
|
||||||
// Vorherige Vorschläge entfernen
|
// Vorherige Vorschläge entfernen
|
||||||
this.suggestionArea.innerHTML = '';
|
this.suggestionArea.innerHTML = '';
|
||||||
|
|
||||||
if (suggestions && suggestions.length > 0) {
|
// Neue Vorschläge hinzufügen
|
||||||
suggestions.forEach(suggestion => {
|
suggestions.forEach((text, index) => {
|
||||||
const pill = document.createElement('button');
|
const pill = document.createElement('button');
|
||||||
pill.className = 'suggestion-pill text-sm bg-gray-200 dark:bg-dark-600 hover:bg-gray-300 dark:hover:bg-dark-500 text-gray-800 dark:text-gray-200 rounded-full px-3 py-1 mb-2 transition-colors';
|
pill.className = 'suggestion-pill text-sm px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-all duration-200';
|
||||||
pill.textContent = suggestion;
|
pill.style.animationDelay = `${index * 0.1}s`;
|
||||||
this.suggestionArea.appendChild(pill);
|
pill.textContent = text;
|
||||||
});
|
this.suggestionArea.appendChild(pill);
|
||||||
|
});
|
||||||
this.suggestionArea.classList.remove('hidden');
|
|
||||||
} else {
|
// Vorschlagsbereich anzeigen
|
||||||
this.suggestionArea.classList.add('hidden');
|
this.suggestionArea.classList.remove('hidden');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -409,14 +342,27 @@ class ChatGPTAssistant {
|
|||||||
messages: this.messages
|
messages: this.messages
|
||||||
}),
|
}),
|
||||||
cache: 'no-cache', // Kein Cache verwenden
|
cache: 'no-cache', // Kein Cache verwenden
|
||||||
credentials: 'same-origin' // Cookies senden
|
credentials: 'same-origin', // Cookies senden
|
||||||
|
timeout: 60000 // 60 Sekunden Timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ladeindikator entfernen
|
// Ladeindikator entfernen
|
||||||
this.removeLoadingIndicator();
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Serverfehler: ${response.status} ${response.statusText}`);
|
const errorText = await response.text();
|
||||||
|
let errorMessage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Versuche, die Fehlermeldung zu parsen
|
||||||
|
const errorData = JSON.parse(errorText);
|
||||||
|
errorMessage = errorData.error || `Serverfehler: ${response.status} ${response.statusText}`;
|
||||||
|
} catch {
|
||||||
|
// Bei Parsing-Fehler verwende Standardfehlermeldung
|
||||||
|
errorMessage = `Serverfehler: ${response.status} ${response.statusText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -442,24 +388,45 @@ class ChatGPTAssistant {
|
|||||||
// Ladeindikator entfernen, falls noch vorhanden
|
// Ladeindikator entfernen, falls noch vorhanden
|
||||||
this.removeLoadingIndicator();
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
|
// Spezielle Fehlermeldungen für bestimmte Fehlertypen
|
||||||
|
const errorMessage = error.message || '';
|
||||||
|
let userFriendlyMessage = 'Es gab ein Problem mit der Anfrage.';
|
||||||
|
|
||||||
|
if (errorMessage.includes('timeout') || errorMessage.includes('Zeitüberschreitung')) {
|
||||||
|
userFriendlyMessage = 'Die Antwort hat zu lange gedauert. Der Server ist möglicherweise überlastet.';
|
||||||
|
} else if (errorMessage.includes('500') || errorMessage.includes('Internal Server Error')) {
|
||||||
|
userFriendlyMessage = 'Ein Serverfehler ist aufgetreten. Wir arbeiten an einer Lösung.';
|
||||||
|
} else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
|
||||||
|
userFriendlyMessage = 'Die API-Anfragelimits wurden erreicht. Bitte warte einen Moment.';
|
||||||
|
}
|
||||||
|
|
||||||
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||||
if (this.retryCount < this.maxRetries) {
|
if (this.retryCount < this.maxRetries) {
|
||||||
this.retryCount++;
|
this.retryCount++;
|
||||||
this.addMessage('assistant', 'Es gab ein Problem mit der Anfrage. Ich versuche es erneut...');
|
this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`);
|
||||||
|
|
||||||
// Kurze Verzögerung vor dem erneuten Versuch
|
// Letzte Benutzernachricht speichern für den Wiederholungsversuch
|
||||||
setTimeout(() => {
|
const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user');
|
||||||
// Letzte Benutzernachricht aus dem Messages-Array entfernen
|
if (lastUserMessageIndex >= 0) {
|
||||||
const lastUserMessage = this.messages[this.messages.length - 2].content;
|
const lastUserMessage = this.messages[lastUserMessageIndex].content;
|
||||||
this.messages = this.messages.slice(0, -2); // Entferne Benutzernachricht und Fehlermeldung
|
|
||||||
|
|
||||||
// Erneuter Versand mit gleicher Nachricht
|
// Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie
|
||||||
this.inputField.value = lastUserMessage;
|
const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ...
|
||||||
this.sendMessage();
|
|
||||||
}, 1500);
|
setTimeout(() => {
|
||||||
|
// Entferne Fehlermeldung aus dem Messages-Array, behalte aber die Benutzernachricht
|
||||||
|
this.messages = this.messages.filter(msg =>
|
||||||
|
!(msg.role === 'assistant' && msg.content.includes('versuche es erneut'))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Erneuter Versand mit gleicher Nachricht
|
||||||
|
this.inputField.value = lastUserMessage;
|
||||||
|
this.sendMessage();
|
||||||
|
}, retryDelay);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Maximale Anzahl an Wiederholungsversuchen erreicht
|
// Maximale Anzahl an Wiederholungsversuchen erreicht
|
||||||
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
|
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.');
|
||||||
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -512,26 +479,33 @@ class ChatGPTAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt einen Ladeindikator im Chat an
|
* Zeigt eine Ladeanimation an
|
||||||
*/
|
*/
|
||||||
showLoadingIndicator() {
|
showLoadingIndicator() {
|
||||||
if (!this.chatHistory) return;
|
if (!this.chatHistory) return;
|
||||||
|
|
||||||
// Entferne vorhandenen Ladeindikator (falls vorhanden)
|
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||||
this.removeLoadingIndicator();
|
if (document.getElementById('assistant-loading-indicator')) return;
|
||||||
|
|
||||||
const loadingEl = document.createElement('div');
|
const loadingEl = document.createElement('div');
|
||||||
loadingEl.id = 'assistant-loading';
|
|
||||||
loadingEl.className = 'flex justify-start';
|
loadingEl.className = 'flex justify-start';
|
||||||
|
loadingEl.id = 'assistant-loading-indicator';
|
||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||||
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
|
||||||
|
|
||||||
|
const typingIndicator = document.createElement('div');
|
||||||
|
typingIndicator.className = 'typing-indicator';
|
||||||
|
typingIndicator.innerHTML = `
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
bubble.appendChild(typingIndicator);
|
||||||
loadingEl.appendChild(bubble);
|
loadingEl.appendChild(bubble);
|
||||||
this.chatHistory.appendChild(loadingEl);
|
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
this.chatHistory.appendChild(loadingEl);
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +513,7 @@ class ChatGPTAssistant {
|
|||||||
* Entfernt den Ladeindikator aus dem Chat
|
* Entfernt den Ladeindikator aus dem Chat
|
||||||
*/
|
*/
|
||||||
removeLoadingIndicator() {
|
removeLoadingIndicator() {
|
||||||
const loadingIndicator = document.getElementById('assistant-loading');
|
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||||
if (loadingIndicator) {
|
if (loadingIndicator) {
|
||||||
loadingIndicator.remove();
|
loadingIndicator.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -18,11 +18,11 @@ class NeuralNetworkBackground {
|
|||||||
|
|
||||||
// Standardkonfiguration mit subtileren Werten
|
// Standardkonfiguration mit subtileren Werten
|
||||||
this.config = {
|
this.config = {
|
||||||
nodeCount: 50, // Weniger Knoten
|
nodeCount: 10, // Weniger Knoten
|
||||||
nodeSize: 1.2, // Kleinere Knoten
|
nodeSize: 1.2, // Kleinere Knoten
|
||||||
connectionDistance: 150, // Reduzierte Verbindungsdistanz
|
connectionDistance: 150, // Reduzierte Verbindungsdistanz
|
||||||
connectionOpacity: 0.3, // Sanftere Verbindungslinien
|
connectionOpacity: 0.3, // Sanftere Verbindungslinien
|
||||||
clusterCount: 2, // Weniger Cluster
|
clusterCount: 7, // Weniger Cluster
|
||||||
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
|
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
|
||||||
animationSpeed: 0.25, // Langsamere Animation
|
animationSpeed: 0.25, // Langsamere Animation
|
||||||
flowDensity: 0.05, // Deutlich weniger Flussanimationen
|
flowDensity: 0.05, // Deutlich weniger Flussanimationen
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
46
templates/admin/update_database.html
Normal file
46
templates/admin/update_database.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Datenbank aktualisieren{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-10">
|
||||||
|
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-purple-400 mb-4">Datenbank aktualisieren</h1>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<div class="mb-6 p-4 rounded-lg {{ 'bg-green-800 bg-opacity-50' if success else 'bg-red-800 bg-opacity-50' }}">
|
||||||
|
<p class="text-white">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-gray-300 mb-4">
|
||||||
|
Diese Funktion aktualisiert die Datenbankstruktur, um mit dem aktuellen Datenmodell kompatibel zu sein.
|
||||||
|
Dabei werden folgende Änderungen vorgenommen:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="list-disc pl-6 text-gray-300 mb-6">
|
||||||
|
<li>Hinzufügen von <code>bio</code>, <code>location</code>, <code>website</code>, <code>avatar</code> und <code>last_login</code> zur Benutzer-Tabelle</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="bg-yellow-800 bg-opacity-30 p-4 rounded-lg mb-6">
|
||||||
|
<p class="text-yellow-200">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||||
|
<strong>Warnung:</strong> Bitte stelle sicher, dass du ein Backup der Datenbank erstellt hast, bevor du fortfährst.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('admin_update_database') }}">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
|
||||||
|
Zurück zur Startseite
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-purple-700 text-white rounded-lg hover:bg-purple-600">
|
||||||
|
Datenbank aktualisieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -6,8 +6,7 @@
|
|||||||
<title>Systades - {% block title %}{% endblock %}</title>
|
<title>Systades - {% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/svg+xml">
|
<link rel="icon" href="{{ url_for('static', filename='img/neuron-favicon.svg') }}" type="image/svg+xml">
|
||||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
|
||||||
|
|
||||||
<!-- Meta Tags -->
|
<!-- Meta Tags -->
|
||||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||||
@@ -59,6 +58,20 @@
|
|||||||
800: '#0e1220',
|
800: '#0e1220',
|
||||||
900: '#0a0e19'
|
900: '#0a0e19'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
float: {
|
||||||
|
'0%, 100%': { transform: 'translateY(0)' },
|
||||||
|
'50%': { transform: 'translateY(-5px)' }
|
||||||
|
},
|
||||||
|
'bounce-slow': {
|
||||||
|
'0%, 100%': { transform: 'translateY(0)' },
|
||||||
|
'50%': { transform: 'translateY(-8px)' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
float: 'float 3s ease-in-out infinite',
|
||||||
|
'bounce-slow': 'bounce-slow 2s ease-in-out infinite'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,8 +82,8 @@
|
|||||||
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Icons - Self-hosted Font Awesome -->
|
<!-- Font Awesome vom CDN -->
|
||||||
<link href="{{ url_for('static', filename='css/all.min.css') }}" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Assistent CSS -->
|
<!-- Assistent CSS -->
|
||||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||||
@@ -87,6 +100,9 @@
|
|||||||
<!-- Neural Network Background CSS -->
|
<!-- Neural Network Background CSS -->
|
||||||
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Mindmap CSS -->
|
||||||
|
<link href="{{ url_for('static', filename='css/mindmap.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- D3.js für Visualisierungen -->
|
<!-- D3.js für Visualisierungen -->
|
||||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
|
|
||||||
@@ -111,18 +127,20 @@
|
|||||||
<!-- Seitenspezifische Styles -->
|
<!-- Seitenspezifische Styles -->
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
<!-- Custom dark mode styles -->
|
<!-- Custom dark/light mode styles -->
|
||||||
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||||
<style>
|
<style>
|
||||||
/* Light‑Mode */
|
/* Light‑Mode */
|
||||||
:root {
|
:root {
|
||||||
--bg-primary:#f4f6fa;
|
--bg-primary:#f8fafc;
|
||||||
--bg-secondary:#e9ecf3;
|
--bg-secondary:#f1f5f9;
|
||||||
--text-primary:#232837;
|
--text-primary:#232837;
|
||||||
--text-secondary:#475569;
|
--text-secondary:#475569;
|
||||||
--accent-primary:#7c3aed;
|
--accent-primary:#7c3aed;
|
||||||
--accent-secondary:#8b5cf6;
|
--accent-secondary:#8b5cf6;
|
||||||
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
||||||
|
background-image: linear-gradient(to bottom right, rgba(248, 250, 252, 0.8), rgba(241, 245, 249, 0.8));
|
||||||
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
/* Dark‑Mode */
|
/* Dark‑Mode */
|
||||||
.dark {
|
.dark {
|
||||||
@@ -136,7 +154,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)] transition-colors duration-300;
|
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)];
|
||||||
|
transition: background-color 0.5s ease-in-out, color 0.3s ease-in-out, background-image 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
@@ -149,6 +168,151 @@
|
|||||||
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
|
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
|
||||||
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
|
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
|
||||||
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
|
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
|
||||||
|
|
||||||
|
/* Light-Mode spezifische Stile */
|
||||||
|
body:not(.dark) {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: rgba(126, 34, 206, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light-active {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
background-color: rgba(126, 34, 206, 0.15);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kartendesign im Light-Mode */
|
||||||
|
body:not(.dark) .card {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .card:hover {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--light-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Buttons */
|
||||||
|
body:not(.dark) .btn,
|
||||||
|
body:not(.dark) button:not(.toggle) {
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
|
color: white !important;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(124, 58, 237, 0.25);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.625rem 1.25rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn:hover,
|
||||||
|
body:not(.dark) button:not(.toggle):hover {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KI-Chat Button im Light-Mode */
|
||||||
|
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"] {
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 500;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"]:hover {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #6366f1);
|
||||||
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style improvements for the theme toggle button */
|
||||||
|
.theme-toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .theme-toggle {
|
||||||
|
background: linear-gradient(to right, #7c3aed, #3b82f6);
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 10px rgba(124, 58, 237, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .theme-toggle {
|
||||||
|
background: linear-gradient(to right, #8b5cf6, #60a5fa);
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 10px rgba(124, 58, 237, 0.15);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .theme-toggle::after {
|
||||||
|
background: #f1f5f9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' width='14' height='14'%3E%3Cpath d='M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||||
|
transform: translateX(24px);
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .theme-toggle::after {
|
||||||
|
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b' width='14' height='14'%3E%3Cpath d='M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
||||||
|
transform: translateX(2px);
|
||||||
|
box-shadow: 0 0 8px rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover::after {
|
||||||
|
box-shadow: 0 0 12px rgba(124, 58, 237, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixes for light mode button text colors */
|
||||||
|
body:not(.dark) .btn-primary {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for KI-Chat container */
|
||||||
|
#chatgpt-assistant {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant {
|
||||||
|
max-height: 80vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-assistant .chat-messages {
|
||||||
|
max-height: calc(80vh - 160px) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||||
@@ -158,6 +322,17 @@
|
|||||||
showSettingsModal: false,
|
showSettingsModal: false,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.initDarkMode();
|
||||||
|
},
|
||||||
|
|
||||||
|
initDarkMode() {
|
||||||
|
// Lade zuerst den Wert aus dem localStorage (client-seitig)
|
||||||
|
const storedMode = localStorage.getItem('colorMode');
|
||||||
|
if (storedMode) {
|
||||||
|
this.darkMode = storedMode === 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dann hole die Server-Einstellung, die Vorrang hat
|
||||||
this.fetchDarkModeFromSession();
|
this.fetchDarkModeFromSession();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -167,7 +342,7 @@
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
this.darkMode = data.darkMode === 'true';
|
this.darkMode = data.darkMode === 'true';
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
this.applyDarkMode();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -175,10 +350,17 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
applyDarkMode() {
|
||||||
|
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||||
|
document.querySelector('body').classList.toggle('dark', this.darkMode);
|
||||||
|
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
|
||||||
|
},
|
||||||
|
|
||||||
toggleDarkMode() {
|
toggleDarkMode() {
|
||||||
this.darkMode = !this.darkMode;
|
this.darkMode = !this.darkMode;
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
this.applyDarkMode();
|
||||||
|
|
||||||
|
// Server über Änderung informieren
|
||||||
fetch('/api/set_dark_mode', {
|
fetch('/api/set_dark_mode', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -189,12 +371,10 @@
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
// Event auslösen für andere Komponenten
|
||||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
detail: { isDark: this.darkMode }
|
detail: { isDark: this.darkMode }
|
||||||
}));
|
}));
|
||||||
} else {
|
|
||||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -210,6 +390,7 @@
|
|||||||
<div class="container mx-auto flex justify-between items-center">
|
<div class="container mx-auto flex justify-between items-center">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<a href="{{ url_for('index') }}" class="flex items-center group">
|
<a href="{{ url_for('index') }}" class="flex items-center group">
|
||||||
|
<img src="{{ url_for('static', filename='img/neuron-logo.svg') }}" alt="Systades Logo" class="w-8 h-8 mr-2 transform transition-transform group-hover:scale-110">
|
||||||
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -241,7 +422,7 @@
|
|||||||
class="nav-link flex items-center"
|
class="nav-link flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
|
||||||
: 'bg-gradient-to-r from-purple-600/30 to-indigo-500/30 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
: 'bg-gradient-to-r from-purple-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
|
||||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -257,25 +438,14 @@
|
|||||||
|
|
||||||
<!-- Rechte Seite -->
|
<!-- Rechte Seite -->
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<!-- Dark Mode Toggle Switch -->
|
<!-- Dark/Light Mode Schalter -->
|
||||||
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
<button
|
||||||
<div class="relative w-12 h-6">
|
@click="toggleDarkMode()"
|
||||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
class="theme-toggle relative w-12 h-6 rounded-full transition-all duration-300 flex items-center overflow-hidden"
|
||||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
aria-label="Dark Mode umschalten"
|
||||||
x-bind:class="darkMode ? 'bg-purple-800/50' : 'bg-gray-400/50'"></div>
|
>
|
||||||
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
|
<span class="sr-only" x-text="darkMode ? 'Zum Light Mode wechseln' : 'Zum Dark Mode wechseln'"></span>
|
||||||
x-bind:class="darkMode ? 'bg-purple-600 transform translate-x-6' : 'bg-white'"></div>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="ml-3 hidden sm:block"
|
|
||||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
|
||||||
<span x-text="darkMode ? 'Dunkel' : 'Hell'"></span>
|
|
||||||
</div>
|
|
||||||
<div class="ml-2 sm:hidden"
|
|
||||||
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
|
||||||
<i class="fa-solid" :class="darkMode ? 'fa-sun' : 'fa-moon'"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Profil-Link oder Login -->
|
<!-- Profil-Link oder Login -->
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<div class="relative" x-data="{ open: false }">
|
<div class="relative" x-data="{ open: false }">
|
||||||
@@ -289,12 +459,21 @@
|
|||||||
{% if current_user.avatar %}
|
{% if current_user.avatar %}
|
||||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ current_user.username[0].upper() }}
|
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#user-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="user-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
<span class="hidden md:block">{{ current_user.username }}</span>
|
||||||
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
|
||||||
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Dropdown-Menü -->
|
<!-- Dropdown-Menü -->
|
||||||
@@ -412,7 +591,7 @@
|
|||||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
||||||
: 'bg-gradient-to-r from-purple-500/10 to-blue-400/10 text-gray-900 hover:from-purple-500/20 hover:to-blue-400/20'">
|
: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
|
||||||
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -478,6 +657,10 @@
|
|||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Mindmap
|
Mindmap
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('search_thoughts_page') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Suche
|
||||||
|
</a>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
@@ -560,6 +743,186 @@
|
|||||||
|
|
||||||
<!-- KI-Chat Initialisierung -->
|
<!-- KI-Chat Initialisierung -->
|
||||||
<script>
|
<script>
|
||||||
|
// ChatGPT-Assistent Klasse
|
||||||
|
class ChatGPTAssistant {
|
||||||
|
constructor() {
|
||||||
|
this.chatContainer = null;
|
||||||
|
this.messages = [];
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Chat-Container erstellen, falls noch nicht vorhanden
|
||||||
|
if (!document.getElementById('chat-assistant-container')) {
|
||||||
|
this.createChatInterface();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Chat-Button
|
||||||
|
const chatButton = document.getElementById('chat-assistant-button');
|
||||||
|
if (chatButton) {
|
||||||
|
chatButton.addEventListener('click', () => this.toggleChat());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Senden-Button
|
||||||
|
const sendButton = document.getElementById('chat-send-button');
|
||||||
|
if (sendButton) {
|
||||||
|
sendButton.addEventListener('click', () => this.sendMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Eingabefeld (Enter-Taste)
|
||||||
|
const inputField = document.getElementById('chat-input');
|
||||||
|
if (inputField) {
|
||||||
|
inputField.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('KI-Assistent erfolgreich initialisiert');
|
||||||
|
}
|
||||||
|
|
||||||
|
createChatInterface() {
|
||||||
|
// Chat-Button erstellen
|
||||||
|
const chatButton = document.createElement('button');
|
||||||
|
chatButton.id = 'chat-assistant-button';
|
||||||
|
chatButton.className = 'fixed bottom-6 right-6 bg-primary-600 text-white rounded-full p-4 shadow-lg z-50 hover:bg-primary-700 transition-all';
|
||||||
|
chatButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
|
||||||
|
document.body.appendChild(chatButton);
|
||||||
|
|
||||||
|
// Chat-Container erstellen
|
||||||
|
const chatContainer = document.createElement('div');
|
||||||
|
chatContainer.id = 'chat-assistant-container';
|
||||||
|
chatContainer.className = 'fixed bottom-24 right-6 w-80 md:w-96 bg-white dark:bg-gray-800 rounded-xl shadow-xl z-50 flex flex-col transition-all duration-300 transform scale-0 origin-bottom-right';
|
||||||
|
chatContainer.style.height = '500px';
|
||||||
|
chatContainer.style.maxHeight = '70vh';
|
||||||
|
|
||||||
|
// Chat-Header
|
||||||
|
chatContainer.innerHTML = `
|
||||||
|
<div class="p-4 border-b dark:border-gray-700 flex justify-between items-center">
|
||||||
|
<h3 class="font-bold text-gray-800 dark:text-white">Systades Assistent</h3>
|
||||||
|
<button id="chat-close-button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4"></div>
|
||||||
|
<div class="p-4 border-t dark:border-gray-700">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<input id="chat-input" type="text" placeholder="Frage stellen..." class="flex-1 px-4 py-2 rounded-lg border dark:border-gray-700 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||||
|
<button id="chat-send-button" class="bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700 transition-all">
|
||||||
|
<i class="fas fa-paper-plane"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(chatContainer);
|
||||||
|
this.chatContainer = chatContainer;
|
||||||
|
|
||||||
|
// Event-Listener für Schließen-Button
|
||||||
|
const closeButton = document.getElementById('chat-close-button');
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', () => this.toggleChat());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleChat() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.chatContainer.classList.remove('scale-0');
|
||||||
|
this.chatContainer.classList.add('scale-100');
|
||||||
|
} else {
|
||||||
|
this.chatContainer.classList.remove('scale-100');
|
||||||
|
this.chatContainer.classList.add('scale-0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage() {
|
||||||
|
const inputField = document.getElementById('chat-input');
|
||||||
|
const messageText = inputField.value.trim();
|
||||||
|
|
||||||
|
if (!messageText) return;
|
||||||
|
|
||||||
|
// Benutzer-Nachricht anzeigen
|
||||||
|
this.addMessage('user', messageText);
|
||||||
|
inputField.value = '';
|
||||||
|
|
||||||
|
// Lade-Indikator anzeigen
|
||||||
|
this.addMessage('assistant', '...', 'loading-message');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API-Anfrage senden
|
||||||
|
const response = await fetch('/api/assistant', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
messages: this.messages.map(msg => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Lade-Nachricht entfernen
|
||||||
|
const loadingMessage = document.getElementById('loading-message');
|
||||||
|
if (loadingMessage) {
|
||||||
|
loadingMessage.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten: ' + data.error);
|
||||||
|
} else {
|
||||||
|
this.addMessage('assistant', data.response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler bei der API-Anfrage:', error);
|
||||||
|
|
||||||
|
// Lade-Nachricht entfernen
|
||||||
|
const loadingMessage = document.getElementById('loading-message');
|
||||||
|
if (loadingMessage) {
|
||||||
|
loadingMessage.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(role, content, id = null) {
|
||||||
|
const messagesContainer = document.getElementById('chat-messages');
|
||||||
|
|
||||||
|
// Nachricht zum Array hinzufügen (außer Lade-Nachrichten)
|
||||||
|
if (id !== 'loading-message') {
|
||||||
|
this.messages.push({ role, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nachricht zum DOM hinzufügen
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
messageElement.className = `p-3 rounded-lg ${role === 'user' ? 'bg-primary-100 dark:bg-primary-900/30 ml-6' : 'bg-gray-100 dark:bg-gray-700 mr-6'}`;
|
||||||
|
if (id) {
|
||||||
|
messageElement.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageElement.innerHTML = `
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center ${role === 'user' ? 'bg-primary-600' : 'bg-gray-600'} text-white mr-2">
|
||||||
|
<i class="fas ${role === 'user' ? 'fa-user' : 'fa-robot'} text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-sm ${role === 'user' ? 'text-gray-800 dark:text-gray-200' : 'text-gray-700 dark:text-gray-300'}">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
messagesContainer.appendChild(messageElement);
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
||||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@@ -578,37 +941,83 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Dark/Light-Mode persistent und robust -->
|
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
// Globaler Zugriff für externe Skripte
|
||||||
function applyMode(mode) {
|
window.MindMap = window.MindMap || {};
|
||||||
if (mode === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark');
|
// Funktion zum Anwenden des Dark Mode, strikt getrennt
|
||||||
localStorage.setItem('colorMode', 'dark');
|
function applyDarkModeClasses(isDarkMode) {
|
||||||
} else {
|
if (isDarkMode) {
|
||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.add('dark');
|
||||||
localStorage.setItem('colorMode', 'light');
|
document.body.classList.add('dark');
|
||||||
}
|
localStorage.setItem('colorMode', 'dark');
|
||||||
}
|
|
||||||
// Beim Laden: Präferenz aus localStorage oder System übernehmen
|
|
||||||
const stored = localStorage.getItem('colorMode');
|
|
||||||
if (stored === 'dark' || stored === 'light') {
|
|
||||||
applyMode(stored);
|
|
||||||
} else {
|
} else {
|
||||||
// Systempräferenz als Fallback
|
document.documentElement.classList.remove('dark');
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
document.body.classList.remove('dark');
|
||||||
applyMode(prefersDark ? 'dark' : 'light');
|
localStorage.setItem('colorMode', 'light');
|
||||||
}
|
}
|
||||||
// Umschalter für alle Mode-Toggles
|
|
||||||
window.toggleColorMode = function() {
|
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
const appEl = document.querySelector('body');
|
||||||
applyMode(isDark ? 'light' : 'dark');
|
if (appEl && appEl.__x) {
|
||||||
};
|
appEl.__x.$data.darkMode = isDarkMode;
|
||||||
// Optional: globales Event für andere Skripte
|
}
|
||||||
window.addEventListener('storage', function(e) {
|
|
||||||
if (e.key === 'colorMode') applyMode(e.newValue);
|
// Event für andere Komponenten auslösen
|
||||||
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
|
detail: { isDark: isDarkMode }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.MindMap.toggleDarkMode = function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
const newIsDark = !isDark;
|
||||||
|
|
||||||
|
// DOM aktualisieren
|
||||||
|
applyDarkModeClasses(newIsDark);
|
||||||
|
|
||||||
|
// Server aktualisieren
|
||||||
|
fetch('/api/set_dark_mode', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ darkMode: newIsDark })
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialisierung beim Laden
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Reihenfolge der Prüfungen: Serverseitige Einstellung > Lokale Einstellung > Browser-Präferenz
|
||||||
|
|
||||||
|
// 1. Zuerst lokale Einstellung prüfen
|
||||||
|
const storedMode = localStorage.getItem('colorMode');
|
||||||
|
if (storedMode) {
|
||||||
|
applyDarkModeClasses(storedMode === 'dark');
|
||||||
|
} else {
|
||||||
|
// 2. Fallback auf Browser-Präferenz
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
applyDarkModeClasses(prefersDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Serverseitige Einstellung abrufen und anwenden
|
||||||
|
fetch('/api/get_dark_mode')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const serverDarkMode = data.darkMode === true || data.darkMode === 'true';
|
||||||
|
applyDarkModeClasses(serverDarkMode);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Fehler beim Abrufen des Dark Mode Status:', error));
|
||||||
|
|
||||||
|
// Listener für Änderungen der Browser-Präferenz
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||||
|
if (localStorage.getItem('colorMode') === null) {
|
||||||
|
applyDarkModeClasses(e.matches);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
192
templates/community/category.html
Normal file
192
templates/community/category.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ category.title }} - Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.thread-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.thread-item:hover {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
.thread-pinned {
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium">{{ category.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Header -->
|
||||||
|
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
|
||||||
|
style="background-color: {{ node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
|
||||||
|
<p class="opacity-75">{{ category.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Neues Thema erstellen -->
|
||||||
|
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Neues Thema
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Threads anzeigen -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<div class="col-span-7 font-medium">Thema</div>
|
||||||
|
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
|
||||||
|
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
|
||||||
|
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thread-Liste -->
|
||||||
|
{% if threads_data %}
|
||||||
|
{% for thread_data in threads_data %}
|
||||||
|
{% set thread = thread_data.thread %}
|
||||||
|
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
|
||||||
|
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
|
||||||
|
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<!-- Thema -->
|
||||||
|
<div class="col-span-12 md:col-span-7">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Status-Icons -->
|
||||||
|
<div class="flex flex-col items-center mr-3 pt-1">
|
||||||
|
{% if thread.is_pinned %}
|
||||||
|
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% if thread.is_locked %}
|
||||||
|
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Themen-Info -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
|
||||||
|
{{ thread.title }}
|
||||||
|
</h3>
|
||||||
|
<div class="flex items-center text-xs opacity-70 mt-1">
|
||||||
|
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
|
||||||
|
<span class="mx-2 block md:hidden">•</span>
|
||||||
|
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antworten -->
|
||||||
|
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
|
||||||
|
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-900/40 text-indigo-300'
|
||||||
|
: 'bg-indigo-100 text-indigo-800'">
|
||||||
|
{{ thread_data.reply_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor -->
|
||||||
|
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if thread.author.avatar %}
|
||||||
|
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
{{ thread.author.username[0].upper() }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Letzte Antwort -->
|
||||||
|
<div class="col-span-2 text-center hidden md:block text-sm">
|
||||||
|
{% if thread_data.latest_reply %}
|
||||||
|
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
|
||||||
|
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="opacity-60">Keine Antworten</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="p-8 text-center">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
|
||||||
|
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
|
||||||
|
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
||||||
|
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Erstes Thema erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link zur Mindmap -->
|
||||||
|
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||||
|
<div class="text-3xl mr-4 opacity-80">
|
||||||
|
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
|
||||||
|
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<a href="{{ url_for('mindmap') }}"
|
||||||
|
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||||
|
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||||
|
Zur Mindmap
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
344
templates/community/edit_post.html
Normal file
344
templates/community/edit_post.html
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Beitrag bearbeiten{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.markdown-preview {
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.markdown-preview p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
||||||
|
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.markdown-preview h1 { font-size: 1.8rem; }
|
||||||
|
.markdown-preview h2 { font-size: 1.5rem; }
|
||||||
|
.markdown-preview h3 { font-size: 1.3rem; }
|
||||||
|
.markdown-preview h4 { font-size: 1.1rem; }
|
||||||
|
.markdown-preview ul, .markdown-preview ol {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview ul { list-style-type: disc; }
|
||||||
|
.markdown-preview ol { list-style-type: decimal; }
|
||||||
|
.markdown-preview pre {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.markdown-preview code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.markdown-preview pre code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.markdown-preview blockquote {
|
||||||
|
border-left: 4px solid;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.dark .markdown-preview code {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-preview blockquote {
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.node-mention {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(109, 40, 217, 0.1);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dark .node-mention {
|
||||||
|
background-color: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<a href="{{ url_for('forum_category', category_id=post.category_id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
{{ post.category.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
{% if post.parent_id %}
|
||||||
|
<a href="{{ url_for('forum_post', post_id=post.parent_id) }}" class="opacity-75 hover:opacity-100 transition-opacity truncate max-w-[200px]">
|
||||||
|
{{ post.parent.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="font-medium">Beitrag bearbeiten</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular-Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">Beitrag bearbeiten</h1>
|
||||||
|
<p class="opacity-75">
|
||||||
|
{% if post.parent_id %}
|
||||||
|
Antwort auf <span class="font-medium">{{ post.parent.title }}</span>
|
||||||
|
{% else %}
|
||||||
|
in der Kategorie <span class="font-medium">{{ post.category.title }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-edit mr-2"></i>
|
||||||
|
Beitrag bearbeiten
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ url_for('edit_post', post_id=post.id) }}" method="POST" x-data="{
|
||||||
|
title: '{{ post.title|safe }}',
|
||||||
|
content: '{{ post.content|replace('\n', '\\n')|replace('\'', '\\\'')|safe }}',
|
||||||
|
showPreview: false,
|
||||||
|
previewHtml: '',
|
||||||
|
|
||||||
|
updatePreview() {
|
||||||
|
// Verarbeite den Inhalt
|
||||||
|
if (this.content.trim() === '') {
|
||||||
|
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verarbeite Markdown
|
||||||
|
let html = marked.parse(this.content);
|
||||||
|
|
||||||
|
// Ersetze @Knotenname mit entsprechenden Links
|
||||||
|
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
||||||
|
|
||||||
|
this.previewHtml = html;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="title" class="block mb-2 font-medium">Titel</label>
|
||||||
|
<div class="rounded-lg overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||||
|
<input type="text" id="title" name="title"
|
||||||
|
class="w-full px-4 py-3"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
x-model="title"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label for="content" class="font-medium">Inhalt</label>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="showPreview = false"
|
||||||
|
x-bind:disabled="!showPreview"
|
||||||
|
x-bind:class="{'opacity-50': !showPreview}">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="updatePreview(); showPreview = true"
|
||||||
|
x-bind:disabled="showPreview"
|
||||||
|
x-bind:class="{'opacity-50': showPreview}">
|
||||||
|
<i class="fas fa-eye mr-1"></i> Vorschau
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
||||||
|
x-show="!showPreview">
|
||||||
|
<textarea id="content" name="content" rows="12"
|
||||||
|
class="w-full p-3 resize-y"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
x-model="content"
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border border-white/20 bg-gray-700/30'
|
||||||
|
: 'border border-gray-300 bg-gray-50'"
|
||||||
|
x-show="showPreview"
|
||||||
|
x-html="previewHtml">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Markdown-Hilfsmittel -->
|
||||||
|
<div class="mb-4" x-show="!showPreview">
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||||
|
<i class="fas fa-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||||
|
<i class="fas fa-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||||
|
<i class="fas fa-quote-right"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||||
|
<i class="fas fa-list-ol"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||||
|
<i class="fas fa-heading"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<a href="{{ url_for('forum_post', post_id=post.parent_id or post.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||||
|
Abbrechen
|
||||||
|
</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-save mr-2"></i>
|
||||||
|
Änderungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Markdown-Buttons für den Beitragseditor
|
||||||
|
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textarea = document.getElementById('content');
|
||||||
|
const format = this.dataset.format;
|
||||||
|
const before = this.dataset.before || '';
|
||||||
|
const after = this.dataset.after || '';
|
||||||
|
|
||||||
|
// Hole die aktuelle Auswahl
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const selection = textarea.value.substring(start, end);
|
||||||
|
|
||||||
|
// Wende die Formatierung an
|
||||||
|
let formattedText;
|
||||||
|
if (format.includes('\n')) {
|
||||||
|
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||||
|
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||||
|
} else if (format.includes('[Link-Text](URL)')) {
|
||||||
|
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||||
|
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||||
|
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||||
|
const beforeSelection = textarea.value.substring(0, start);
|
||||||
|
const afterSelection = textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Finde den Anfang der aktuellen Zeile
|
||||||
|
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||||
|
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Füge das Format am Zeilenanfang ein
|
||||||
|
formattedText = beforeSelection.substring(0, lineStart) +
|
||||||
|
format +
|
||||||
|
beforeSelection.substring(lineStart) +
|
||||||
|
selection +
|
||||||
|
afterSelection;
|
||||||
|
|
||||||
|
// Setze die neue Cursor-Position
|
||||||
|
const newCursorPos = end + format.length;
|
||||||
|
textarea.value = formattedText;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||||
|
} else {
|
||||||
|
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||||
|
formattedText = before + format + selection + format + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersetze den Text
|
||||||
|
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Setze den Fokus zurück auf das Textarea
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||||
|
if (selection) {
|
||||||
|
const newStart = start + before.length + format.length;
|
||||||
|
const newEnd = newStart + selection.length;
|
||||||
|
textarea.setSelectionRange(newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||||
|
const newCursorPos = start + before.length + format.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
125
templates/community/index.html
Normal file
125
templates/community/index.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Community Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.forum-category {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.forum-category:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.category-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Seitenüberschrift -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
|
||||||
|
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forumskategorien -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
{% if categories_data %}
|
||||||
|
{% for cat_data in categories_data %}
|
||||||
|
<a href="{{ url_for('forum_category', category_id=cat_data.category.id) }}" class="forum-category block">
|
||||||
|
<div class="rounded-xl p-5 h-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 hover:bg-gray-800/80 border border-white/10' : 'bg-white hover:bg-gray-50 border border-gray-200 shadow-md'">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="category-icon mr-4 text-white"
|
||||||
|
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
|
||||||
|
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
|
||||||
|
|
||||||
|
<!-- Statistik -->
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm opacity-80">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-comment-alt mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_posts }} Themen</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-reply mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_replies }} Antworten</span>
|
||||||
|
</div>
|
||||||
|
{% if cat_data.latest_post %}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-clock mr-2"></i>
|
||||||
|
<span>Neuster Beitrag: {{ cat_data.latest_post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pfeil-Icon -->
|
||||||
|
<div class="ml-2">
|
||||||
|
<i class="fas fa-chevron-right opacity-50"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-span-2 text-center py-8">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
|
||||||
|
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hinweis zur Nutzung -->
|
||||||
|
<div class="rounded-xl p-6 text-center mb-8"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">
|
||||||
|
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
|
||||||
|
So funktioniert das Forum
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
|
||||||
|
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
|
||||||
|
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
|
||||||
|
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Markdown Support</h4>
|
||||||
|
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
355
templates/community/new_post.html
Normal file
355
templates/community/new_post.html
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Neues Thema - {{ category.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.markdown-preview {
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.markdown-preview p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
||||||
|
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.markdown-preview h1 { font-size: 1.8rem; }
|
||||||
|
.markdown-preview h2 { font-size: 1.5rem; }
|
||||||
|
.markdown-preview h3 { font-size: 1.3rem; }
|
||||||
|
.markdown-preview h4 { font-size: 1.1rem; }
|
||||||
|
.markdown-preview ul, .markdown-preview ol {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.markdown-preview ul { list-style-type: disc; }
|
||||||
|
.markdown-preview ol { list-style-type: decimal; }
|
||||||
|
.markdown-preview pre {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.markdown-preview code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.markdown-preview pre code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.markdown-preview blockquote {
|
||||||
|
border-left: 4px solid;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.dark .markdown-preview code {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .markdown-preview blockquote {
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.node-mention {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(109, 40, 217, 0.1);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dark .node-mention {
|
||||||
|
background-color: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
{{ category.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium">Neues Thema</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular-Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">Neues Thema erstellen</h1>
|
||||||
|
<p class="opacity-75">in der Kategorie <span class="font-medium">{{ category.title }}</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formular -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-plus-circle mr-2"></i>
|
||||||
|
Neues Thema
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ url_for('new_post', category_id=category.id) }}" method="POST" x-data="{
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
showPreview: false,
|
||||||
|
previewHtml: '',
|
||||||
|
|
||||||
|
updatePreview() {
|
||||||
|
// Verarbeite den Inhalt
|
||||||
|
if (this.content.trim() === '') {
|
||||||
|
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verarbeite Markdown
|
||||||
|
let html = marked.parse(this.content);
|
||||||
|
|
||||||
|
// Ersetze @Knotenname mit entsprechenden Links
|
||||||
|
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
||||||
|
|
||||||
|
this.previewHtml = html;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="title" class="block mb-2 font-medium">Titel des Themas</label>
|
||||||
|
<div class="rounded-lg overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||||
|
<input type="text" id="title" name="title"
|
||||||
|
class="w-full px-4 py-3"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
placeholder="Ein prägnanter Titel für dein Thema"
|
||||||
|
x-model="title"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label for="content" class="font-medium">Inhalt</label>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="showPreview = false"
|
||||||
|
x-bind:disabled="!showPreview"
|
||||||
|
x-bind:class="{'opacity-50': !showPreview}">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="px-3 py-1 rounded text-sm flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
||||||
|
@click="updatePreview(); showPreview = true"
|
||||||
|
x-bind:disabled="showPreview"
|
||||||
|
x-bind:class="{'opacity-50': showPreview}">
|
||||||
|
<i class="fas fa-eye mr-1"></i> Vorschau
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
||||||
|
x-show="!showPreview">
|
||||||
|
<textarea id="content" name="content" rows="12"
|
||||||
|
class="w-full p-3 resize-y"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
placeholder="Schreibe deinen Beitrag hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
||||||
|
x-model="content"
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'border border-white/20 bg-gray-700/30'
|
||||||
|
: 'border border-gray-300 bg-gray-50'"
|
||||||
|
x-show="showPreview"
|
||||||
|
x-html="previewHtml">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Markdown-Hilfsmittel -->
|
||||||
|
<div class="mb-4" x-show="!showPreview">
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||||
|
<i class="fas fa-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||||
|
<i class="fas fa-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||||
|
<i class="fas fa-quote-right"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||||
|
<i class="fas fa-list-ol"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||||
|
<i class="fas fa-heading"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<a href="{{ url_for('forum_category', category_id=category.id) }}"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||||
|
Abbrechen
|
||||||
|
</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-paper-plane mr-2"></i>
|
||||||
|
Thema erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link zur Mindmap -->
|
||||||
|
<div class="rounded-xl p-5 mb-4 flex items-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
||||||
|
<div class="text-3xl mr-4 opacity-80">
|
||||||
|
<i class="fas fa-diagram-project" style="color: {{ category.node.color_code }}"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ category.node.name }}</h3>
|
||||||
|
<p class="text-sm opacity-75">Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<a href="{{ url_for('mindmap') }}"
|
||||||
|
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
||||||
|
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
||||||
|
Zur Mindmap
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Markdown-Buttons für den Beitragseditor
|
||||||
|
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textarea = document.getElementById('content');
|
||||||
|
const format = this.dataset.format;
|
||||||
|
const before = this.dataset.before || '';
|
||||||
|
const after = this.dataset.after || '';
|
||||||
|
|
||||||
|
// Hole die aktuelle Auswahl
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const selection = textarea.value.substring(start, end);
|
||||||
|
|
||||||
|
// Wende die Formatierung an
|
||||||
|
let formattedText;
|
||||||
|
if (format.includes('\n')) {
|
||||||
|
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||||
|
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||||
|
} else if (format.includes('[Link-Text](URL)')) {
|
||||||
|
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||||
|
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||||
|
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||||
|
const beforeSelection = textarea.value.substring(0, start);
|
||||||
|
const afterSelection = textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Finde den Anfang der aktuellen Zeile
|
||||||
|
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||||
|
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Füge das Format am Zeilenanfang ein
|
||||||
|
formattedText = beforeSelection.substring(0, lineStart) +
|
||||||
|
format +
|
||||||
|
beforeSelection.substring(lineStart) +
|
||||||
|
selection +
|
||||||
|
afterSelection;
|
||||||
|
|
||||||
|
// Setze die neue Cursor-Position
|
||||||
|
const newCursorPos = end + format.length;
|
||||||
|
textarea.value = formattedText;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||||
|
} else {
|
||||||
|
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||||
|
formattedText = before + format + selection + format + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersetze den Text
|
||||||
|
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Setze den Fokus zurück auf das Textarea
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// Alpine.js Model aktualisieren
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||||
|
if (selection) {
|
||||||
|
const newStart = start + before.length + format.length;
|
||||||
|
const newEnd = newStart + selection.length;
|
||||||
|
textarea.setSelectionRange(newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||||
|
const newCursorPos = start + before.length + format.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
511
templates/community/post.html
Normal file
511
templates/community/post.html
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ post.title }} - Forum{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.post-content {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.post-content p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.post-content h1, .post-content h2, .post-content h3,
|
||||||
|
.post-content h4, .post-content h5, .post-content h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.post-content h1 { font-size: 1.8rem; }
|
||||||
|
.post-content h2 { font-size: 1.5rem; }
|
||||||
|
.post-content h3 { font-size: 1.3rem; }
|
||||||
|
.post-content h4 { font-size: 1.1rem; }
|
||||||
|
.post-content ul, .post-content ol {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.post-content ul { list-style-type: disc; }
|
||||||
|
.post-content ol { list-style-type: decimal; }
|
||||||
|
.post-content pre {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.post-content code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.post-content pre code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.post-content blockquote {
|
||||||
|
border-left: 4px solid;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.post-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.post-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.post-content th, .post-content td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.post-content th {
|
||||||
|
background-color: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.post-content a {
|
||||||
|
color: #6d28d9;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.post-content a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.dark .post-content code {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .post-content th, .dark .post-content td {
|
||||||
|
border-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.dark .post-content th {
|
||||||
|
background-color: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
.dark .post-content a {
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
.node-mention {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(109, 40, 217, 0.1);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.dark .node-mention {
|
||||||
|
background-color: rgba(167, 139, 250, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<div class="mb-6 flex items-center text-sm">
|
||||||
|
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
<i class="fas fa-home mr-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
||||||
|
{{ category.title }}
|
||||||
|
</a>
|
||||||
|
<span class="mx-2 opacity-50">/</span>
|
||||||
|
<span class="font-medium truncate max-w-[300px]">{{ post.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Beitrags-Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">{{ post.title }}</h1>
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-sm opacity-75">
|
||||||
|
<span><i class="fas fa-calendar-alt mr-1"></i> {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
||||||
|
<span><i class="fas fa-eye mr-1"></i> {{ post.view_count }} Aufrufe</span>
|
||||||
|
<span><i class="fas fa-reply mr-1"></i> {{ replies|length }} Antworten</span>
|
||||||
|
|
||||||
|
{% if post.is_pinned or post.is_locked %}
|
||||||
|
<div class="flex gap-2 ml-2">
|
||||||
|
{% if post.is_pinned %}
|
||||||
|
<span class="px-2 py-0.5 text-xs rounded-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-yellow-700/50 text-yellow-300' : 'bg-yellow-100 text-yellow-800'">
|
||||||
|
<i class="fas fa-thumbtack mr-1"></i> Angepinnt
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if post.is_locked %}
|
||||||
|
<span class="px-2 py-0.5 text-xs rounded-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-red-700/50 text-red-300' : 'bg-red-100 text-red-800'">
|
||||||
|
<i class="fas fa-lock mr-1"></i> Gesperrt
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hauptbeitrag -->
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-sm'">
|
||||||
|
<!-- Beitrags-Header -->
|
||||||
|
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Autor-Avatar -->
|
||||||
|
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden mr-3"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if post.author.avatar %}
|
||||||
|
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#post-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="post-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor-Info -->
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ post.author.username }}</div>
|
||||||
|
<div class="text-xs opacity-70">Erstellt am {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktionen -->
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
{% if current_user.id == post.user_id or current_user.role == 'admin' %}
|
||||||
|
<a href="{{ url_for('edit_post', post_id=post.id) }}"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-gray-700/50 text-gray-300'
|
||||||
|
: 'hover:bg-gray-100 text-gray-600'">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ url_for('delete_post', post_id=post.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diesen Beitrag wirklich löschen?');">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-red-800/50 text-red-300'
|
||||||
|
: 'hover:bg-red-100 text-red-600'">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Moderation-Optionen -->
|
||||||
|
{% if current_user.role in ['admin', 'moderator'] %}
|
||||||
|
<div class="ml-2 border-l" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'"></div>
|
||||||
|
<form action="{{ url_for('toggle_pin_post', post_id=post.id) }}" method="POST" class="inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-yellow-800/50 text-yellow-300'
|
||||||
|
: 'hover:bg-yellow-100 text-yellow-600'"
|
||||||
|
title="{% if post.is_pinned %}Nicht mehr anpinnen{% else %}Anpinnen{% endif %}">
|
||||||
|
<i class="fas fa-thumbtack"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form action="{{ url_for('toggle_lock_post', post_id=post.id) }}" method="POST" class="inline">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-blue-800/50 text-blue-300'
|
||||||
|
: 'hover:bg-blue-100 text-blue-600'"
|
||||||
|
title="{% if post.is_locked %}Entsperren{% else %}Sperren{% endif %}">
|
||||||
|
<i class="fas {% if post.is_locked %}fa-unlock{% else %}fa-lock{% endif %}"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Beitrags-Inhalt -->
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="post-content markdown-content" id="main-post-content">
|
||||||
|
{{ post.content|safe }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if post.updated_at and post.updated_at != post.created_at %}
|
||||||
|
<div class="mt-6 pt-4 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ post.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antworten-Bereich -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">
|
||||||
|
<i class="fas fa-reply mr-2 opacity-60"></i>
|
||||||
|
{{ replies|length }} Antworten
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Antworten-Liste -->
|
||||||
|
{% if replies %}
|
||||||
|
{% for reply in replies %}
|
||||||
|
<div class="mb-5 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<!-- Antwort-Header -->
|
||||||
|
<div class="p-3 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Autor-Avatar -->
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white font-medium text-xs overflow-hidden mr-3"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if reply.author.avatar %}
|
||||||
|
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#reply-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="reply-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autor-Info -->
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-sm">{{ reply.author.username }}</div>
|
||||||
|
<div class="text-xs opacity-70">{{ reply.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktionen -->
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
{% if current_user.id == reply.user_id or current_user.role == 'admin' %}
|
||||||
|
<a href="{{ url_for('edit_post', post_id=reply.id) }}"
|
||||||
|
class="p-1.5 rounded text-sm transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-gray-700/50 text-gray-300'
|
||||||
|
: 'hover:bg-gray-100 text-gray-600'">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ url_for('delete_post', post_id=reply.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diese Antwort wirklich löschen?');">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-1.5 rounded text-sm transition-colors"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'hover:bg-red-800/50 text-red-300'
|
||||||
|
: 'hover:bg-red-100 text-red-600'">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antwort-Inhalt -->
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="post-content markdown-content reply-content" id="reply-content-{{ reply.id }}">
|
||||||
|
{{ reply.content|safe }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if reply.updated_at and reply.updated_at != reply.created_at %}
|
||||||
|
<div class="mt-4 pt-3 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ reply.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="rounded-xl p-6 text-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Noch keine Antworten</h3>
|
||||||
|
<p class="opacity-75">Sei der Erste, der auf diesen Beitrag antwortet!</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antwort-Formular -->
|
||||||
|
{% if not post.is_locked %}
|
||||||
|
<div class="mb-8 rounded-xl overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
||||||
|
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
||||||
|
<i class="fas fa-reply mr-2"></i>
|
||||||
|
Antworten
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form action="{{ url_for('reply_to_post', post_id=post.id) }}" method="POST">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="content" class="block mb-2 font-medium">Deine Antwort</label>
|
||||||
|
<div class="mb-2 rounded-lg overflow-hidden"
|
||||||
|
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
||||||
|
<textarea id="content" name="content" rows="6"
|
||||||
|
class="w-full p-3 resize-y"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
||||||
|
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
||||||
|
placeholder="Schreibe deine Antwort hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
||||||
|
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-1">
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
||||||
|
<i class="fas fa-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
||||||
|
<i class="fas fa-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
||||||
|
<i class="fas fa-file-code"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
||||||
|
<i class="fas fa-quote-right"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
||||||
|
<i class="fas fa-list-ol"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
||||||
|
<i class="fas fa-heading"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
||||||
|
<i class="fas fa-paper-plane mr-2"></i>
|
||||||
|
Antwort senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="rounded-xl p-5 text-center mb-6"
|
||||||
|
x-bind:class="darkMode ? 'bg-red-900/20 border border-red-800/30' : 'bg-red-50 border border-red-100'">
|
||||||
|
<i class="fas fa-lock mr-2 text-red-500"></i>
|
||||||
|
<span>Dieser Beitrag ist geschlossen. Es können keine neuen Antworten mehr verfasst werden.</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Markdown und Knotenerwähnungen verarbeiten
|
||||||
|
const processContent = (content) => {
|
||||||
|
// Verarbeite Markdown mit marked.js
|
||||||
|
let html = marked.parse(content);
|
||||||
|
|
||||||
|
// Ersetze @Knotenname mit entsprechenden Links
|
||||||
|
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class="node-mention"><i class="fas fa-diagram-project fa-xs mr-1"></i>$1</span>');
|
||||||
|
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Markdown-Inhalt für Hauptbeitrag rendern
|
||||||
|
const mainPostContent = document.getElementById('main-post-content');
|
||||||
|
if (mainPostContent) {
|
||||||
|
mainPostContent.innerHTML = processContent(mainPostContent.textContent.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown-Inhalt für Antworten rendern
|
||||||
|
document.querySelectorAll('.reply-content').forEach(reply => {
|
||||||
|
reply.innerHTML = processContent(reply.textContent.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Markdown-Buttons für das Antwortformular
|
||||||
|
document.querySelectorAll('.markdown-button').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const textarea = document.getElementById('content');
|
||||||
|
const format = this.dataset.format;
|
||||||
|
const before = this.dataset.before || '';
|
||||||
|
const after = this.dataset.after || '';
|
||||||
|
|
||||||
|
// Hole die aktuelle Auswahl
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const selection = textarea.value.substring(start, end);
|
||||||
|
|
||||||
|
// Wende die Formatierung an
|
||||||
|
let formattedText;
|
||||||
|
if (format.includes('\n')) {
|
||||||
|
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
||||||
|
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
||||||
|
} else if (format.includes('[Link-Text](URL)')) {
|
||||||
|
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
||||||
|
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
||||||
|
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
||||||
|
const beforeSelection = textarea.value.substring(0, start);
|
||||||
|
const afterSelection = textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Finde den Anfang der aktuellen Zeile
|
||||||
|
const lastNewline = beforeSelection.lastIndexOf('\n');
|
||||||
|
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
||||||
|
|
||||||
|
// Füge das Format am Zeilenanfang ein
|
||||||
|
formattedText = beforeSelection.substring(0, lineStart) +
|
||||||
|
format +
|
||||||
|
beforeSelection.substring(lineStart) +
|
||||||
|
selection +
|
||||||
|
afterSelection;
|
||||||
|
|
||||||
|
// Setze die neue Cursor-Position
|
||||||
|
const newCursorPos = end + format.length;
|
||||||
|
textarea.value = formattedText;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
||||||
|
} else {
|
||||||
|
// Für einfache Formatierungen wie fett, kursiv, Code
|
||||||
|
formattedText = before + format + selection + format + after;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersetze den Text
|
||||||
|
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
||||||
|
|
||||||
|
// Setze den Fokus zurück auf das Textarea
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
||||||
|
if (selection) {
|
||||||
|
const newStart = start + before.length + format.length;
|
||||||
|
const newEnd = newStart + selection.length;
|
||||||
|
textarea.setSelectionRange(newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
// Setze den Cursor in die Mitte von **|** oder `|`
|
||||||
|
const newCursorPos = start + before.length + format.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
137
templates/community/preview.html
Normal file
137
templates/community/preview.html
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Community Forum Vorschau{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.forum-category {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.forum-category:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.category-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Seitenüberschrift -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
|
||||||
|
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login-Aufforderung -->
|
||||||
|
<div class="rounded-xl p-6 text-center mb-8 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-100 dark:border-indigo-700/30">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">
|
||||||
|
<i class="fas fa-lock mr-2 text-indigo-500"></i>
|
||||||
|
Anmeldung erforderlich
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">Um am Community-Forum teilzunehmen und alle Funktionen nutzen zu können, musst du dich anmelden oder registrieren.</p>
|
||||||
|
<div class="flex justify-center gap-4 mt-4">
|
||||||
|
<a href="{{ url_for('login', next=url_for('forum')) }}" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-sign-in-alt mr-2"></i>Anmelden
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('register') }}" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg transition-colors">
|
||||||
|
<i class="fas fa-user-plus mr-2"></i>Registrieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forumskategorien Vorschau -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
{% if categories_data %}
|
||||||
|
{% for cat_data in categories_data %}
|
||||||
|
<div class="forum-category block">
|
||||||
|
<div class="rounded-xl p-5 h-full"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-md'">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<!-- Kategorie-Icon -->
|
||||||
|
<div class="category-icon mr-4 text-white"
|
||||||
|
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
|
||||||
|
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorie-Info -->
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
|
||||||
|
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
|
||||||
|
|
||||||
|
<!-- Statistik -->
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm opacity-80">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-comment-alt mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_posts }} Themen</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-reply mr-2"></i>
|
||||||
|
<span>{{ cat_data.total_replies }} Antworten</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pfeil-Icon -->
|
||||||
|
<div class="ml-2">
|
||||||
|
<i class="fas fa-lock opacity-50"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-span-2 text-center py-8">
|
||||||
|
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
|
||||||
|
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hinweis zur Nutzung -->
|
||||||
|
<div class="rounded-xl p-6 text-center mb-8"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3">
|
||||||
|
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
|
||||||
|
So funktioniert das Forum
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
|
||||||
|
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
|
||||||
|
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
|
||||||
|
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
||||||
|
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
|
||||||
|
<h4 class="font-medium mb-1">Markdown Support</h4>
|
||||||
|
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
365
templates/create_mindmap.html
Normal file
365
templates/create_mindmap.html
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Mindmap erstellen{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Spezifische Stile für die Mindmap-Erstellungsseite */
|
||||||
|
.form-container {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-container {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-container {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-header {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input,
|
||||||
|
body.dark .form-textarea {
|
||||||
|
background-color: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-input,
|
||||||
|
body:not(.dark) .form-textarea {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input:focus,
|
||||||
|
body.dark .form-textarea:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-input:focus,
|
||||||
|
body:not(.dark) .form-textarea:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input[type="checkbox"] {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch label {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 50px;
|
||||||
|
height: 25px;
|
||||||
|
background: rgba(100, 116, 139, 0.3);
|
||||||
|
display: block;
|
||||||
|
border-radius: 25px;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch label:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 19px;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input:checked + label {
|
||||||
|
background: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input:checked + label:after {
|
||||||
|
left: calc(100% - 3px);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background-color: #7c3aed;
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
background-color: #6d28d9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-cancel {
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-cancel {
|
||||||
|
color: #475569;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-cancel:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-cancel:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation für den Seiteneintritt */
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
animation: slideInUp 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation für Hover-Effekte */
|
||||||
|
.input-animation {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-animation:focus {
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8 animate-fadeIn">
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<!-- Titel mit Animation -->
|
||||||
|
<div class="text-center mb-8 animate-pulse">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
|
||||||
|
Neue Mindmap erstellen
|
||||||
|
</h1>
|
||||||
|
<p class="opacity-80">Erstelle deine eigene Wissenslandkarte und organisiere deine Gedanken</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="form-header">
|
||||||
|
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-body">
|
||||||
|
<form action="{{ url_for('create_mindmap') }}" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name" class="form-label">Name der Mindmap</label>
|
||||||
|
<input type="text" id="name" name="name" class="form-input input-animation" required placeholder="z.B. Meine Philosophie-Mindmap">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-label">Beschreibung</label>
|
||||||
|
<textarea id="description" name="description" class="form-textarea input-animation" placeholder="Worum geht es in dieser Mindmap?"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-switch">
|
||||||
|
<input type="checkbox" id="is_private" name="is_private" checked>
|
||||||
|
<label for="is_private"></label>
|
||||||
|
<span>Private Mindmap (nur für dich sichtbar)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between mt-6">
|
||||||
|
<a href="{{ url_for('profile') }}" class="btn-cancel">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn-submit">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
Mindmap erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Mindmap-Vorschau -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Vorschau</h3>
|
||||||
|
<div class="mindmap-container">
|
||||||
|
<div id="cy" class="w-full h-[400px] rounded-xl border"
|
||||||
|
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tipps-Sektion -->
|
||||||
|
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
|
||||||
|
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3"
|
||||||
|
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||||
|
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Erstellen einer Mindmap
|
||||||
|
</h3>
|
||||||
|
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
<ul class="list-disc pl-5 space-y-2">
|
||||||
|
<li>Wähle einen prägnanten, aber aussagekräftigen Namen für deine Mindmap</li>
|
||||||
|
<li>Beginne mit einem zentralen Konzept und arbeite dich nach außen vor</li>
|
||||||
|
<li>Verwende verschiedene Farben für unterschiedliche Kategorien oder Themenbereiche</li>
|
||||||
|
<li>Füge Notizen zu Knoten hinzu, um komplexere Ideen zu erklären</li>
|
||||||
|
<li>Verknüpfe verwandte Konzepte, um Beziehungen zu visualisieren</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Einfache Animationen für die Eingabefelder
|
||||||
|
const inputs = document.querySelectorAll('.input-animation');
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
// Subtile Skalierung bei Fokus
|
||||||
|
input.addEventListener('focus', function() {
|
||||||
|
this.style.transform = 'scale(1.01)';
|
||||||
|
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
this.style.transform = 'scale(1)';
|
||||||
|
this.style.boxShadow = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formular-Absenden-Animation
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
const submitBtn = this.querySelector('.btn-submit');
|
||||||
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird erstellt...';
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mindmap-Vorschau initialisieren
|
||||||
|
const mindmap = new MindMap.Visualization('cy', {
|
||||||
|
enableEditing: true,
|
||||||
|
onNodeClick: function(nodeData) {
|
||||||
|
console.log("Knoten ausgewählt:", nodeData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formularfelder mit Mindmap verbinden
|
||||||
|
const nameInput = document.getElementById('name');
|
||||||
|
const descriptionInput = document.getElementById('description');
|
||||||
|
|
||||||
|
// Aktualisiere Mindmap wenn sich die Eingaben ändern
|
||||||
|
nameInput.addEventListener('input', function() {
|
||||||
|
if (mindmap.cy) {
|
||||||
|
const rootNode = mindmap.cy.$('#root');
|
||||||
|
if (rootNode.length > 0) {
|
||||||
|
rootNode.data('name', this.value || 'Neue Mindmap');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialisiere die Mindmap
|
||||||
|
mindmap.initialize().then(() => {
|
||||||
|
console.log("Mindmap-Vorschau initialisiert");
|
||||||
|
|
||||||
|
// Setze initiale Werte
|
||||||
|
if (nameInput.value) {
|
||||||
|
const rootNode = mindmap.cy.$('#root');
|
||||||
|
if (rootNode.length > 0) {
|
||||||
|
rootNode.data('name', nameInput.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Fehler bei der Initialisierung der Mindmap:", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
423
templates/edit_mindmap.html
Normal file
423
templates/edit_mindmap.html
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Mindmap bearbeiten{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Spezifische Stile für die Mindmap-Bearbeitungsseite */
|
||||||
|
.form-container {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-container {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-container {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-header {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input,
|
||||||
|
body.dark .form-textarea {
|
||||||
|
background-color: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-input,
|
||||||
|
body:not(.dark) .form-textarea {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .form-input:focus,
|
||||||
|
body.dark .form-textarea:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .form-input:focus,
|
||||||
|
body:not(.dark) .form-textarea:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input[type="checkbox"] {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch label {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 50px;
|
||||||
|
height: 25px;
|
||||||
|
background: rgba(100, 116, 139, 0.3);
|
||||||
|
display: block;
|
||||||
|
border-radius: 25px;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch label:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 19px;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input:checked + label {
|
||||||
|
background: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch input:checked + label:after {
|
||||||
|
left: calc(100% - 3px);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background-color: #7c3aed;
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
background-color: #6d28d9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-cancel {
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-cancel {
|
||||||
|
color: #475569;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .btn-cancel:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) .btn-cancel:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation für den Seiteneintritt */
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
animation: slideInUp 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation für Hover-Effekte */
|
||||||
|
.input-animation {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-animation:focus {
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8 animate-fadeIn">
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<!-- Titel mit Animation -->
|
||||||
|
<div class="text-center mb-8 animate-pulse">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
|
||||||
|
Mindmap bearbeiten
|
||||||
|
</h1>
|
||||||
|
<p class="opacity-80">Aktualisiere die Details deiner Mindmap</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="form-header">
|
||||||
|
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-body">
|
||||||
|
<form action="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name" class="form-label">Name der Mindmap</label>
|
||||||
|
<input type="text" id="name" name="name" class="form-input input-animation" required
|
||||||
|
placeholder="z.B. Meine Philosophie-Mindmap" value="{{ mindmap.name }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-label">Beschreibung</label>
|
||||||
|
<textarea id="description" name="description" class="form-textarea input-animation"
|
||||||
|
placeholder="Worum geht es in dieser Mindmap?">{{ mindmap.description }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-switch">
|
||||||
|
<input type="checkbox" id="is_private" name="is_private" {% if mindmap.is_private %}checked{% endif %}>
|
||||||
|
<label for="is_private"></label>
|
||||||
|
<span>Private Mindmap (nur für dich sichtbar)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between mt-6">
|
||||||
|
<a href="{{ url_for('mindmap', mindmap_id=mindmap.id) }}" class="btn-cancel">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn-submit">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
Änderungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Mindmap-Editor -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Mindmap bearbeiten</h3>
|
||||||
|
<div class="mindmap-container">
|
||||||
|
<div id="cy" class="w-full h-[600px] rounded-xl border"
|
||||||
|
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Bearbeitungshinweise -->
|
||||||
|
<div class="mt-4 text-sm opacity-80">
|
||||||
|
<p><i class="fas fa-info-circle mr-2"></i>Klicke auf Knoten zum Bearbeiten, ziehe sie zum Neuanordnen oder nutze die Toolbar für weitere Funktionen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tipps-Sektion -->
|
||||||
|
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
|
||||||
|
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3"
|
||||||
|
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||||
|
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Bearbeiten einer Mindmap
|
||||||
|
</h3>
|
||||||
|
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
<ul class="list-disc pl-5 space-y-2">
|
||||||
|
<li>Überprüfe, ob der Name noch zum aktuellen Inhalt passt</li>
|
||||||
|
<li>Aktualisiere die Beschreibung, um neue Aspekte zu berücksichtigen</li>
|
||||||
|
<li>Entscheide, ob die Sichtbarkeitseinstellungen noch passend sind</li>
|
||||||
|
<li>Nutze aussagekräftige Namen für bessere Auffindbarkeit</li>
|
||||||
|
<li>Behalte die Konsistenz mit verknüpften Konzepten im Auge</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Einfache Animationen für die Eingabefelder
|
||||||
|
const inputs = document.querySelectorAll('.input-animation');
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
// Subtile Skalierung bei Fokus
|
||||||
|
input.addEventListener('focus', function() {
|
||||||
|
this.style.transform = 'scale(1.01)';
|
||||||
|
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
this.style.transform = 'scale(1)';
|
||||||
|
this.style.boxShadow = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formular-Absenden-Animation
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
const submitBtn = this.querySelector('.btn-submit');
|
||||||
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mindmap initialisieren
|
||||||
|
const mindmap = new MindMap.Visualization('cy', {
|
||||||
|
enableEditing: true,
|
||||||
|
apiEndpoint: '/api/mindmap/{{ mindmap.id }}',
|
||||||
|
onNodeClick: function(nodeData) {
|
||||||
|
console.log("Knoten ausgewählt:", nodeData);
|
||||||
|
},
|
||||||
|
onChange: function(data) {
|
||||||
|
// Automatisches Speichern bei Änderungen
|
||||||
|
fetch('/api/mindmap/{{ mindmap.id }}/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token() }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}).then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Netzwerkfehler beim Speichern');
|
||||||
|
}
|
||||||
|
console.log('Änderungen gespeichert');
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Fehler beim Speichern:', error);
|
||||||
|
alert('Fehler beim Speichern der Änderungen');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formularfelder mit Mindmap verbinden
|
||||||
|
const nameInput = document.getElementById('name');
|
||||||
|
const descriptionInput = document.getElementById('description');
|
||||||
|
|
||||||
|
// Aktualisiere Mindmap wenn sich die Eingaben ändern
|
||||||
|
nameInput.addEventListener('input', function() {
|
||||||
|
if (mindmap.cy) {
|
||||||
|
const rootNode = mindmap.cy.$('#root');
|
||||||
|
if (rootNode.length > 0) {
|
||||||
|
rootNode.data('name', this.value || 'Mindmap');
|
||||||
|
mindmap.saveToServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialisiere die Mindmap mit existierenden Daten
|
||||||
|
mindmap.initialize().then(() => {
|
||||||
|
console.log("Mindmap-Editor initialisiert");
|
||||||
|
|
||||||
|
// Lade existierende Daten
|
||||||
|
fetch('/api/mindmap/{{ mindmap.id }}/data')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
mindmap.loadData(data);
|
||||||
|
console.log("Mindmap-Daten geladen");
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Fehler beim Laden der Mindmap-Daten:", error);
|
||||||
|
alert("Fehler beim Laden der Mindmap");
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Fehler bei der Initialisierung des Editors:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Autosave-Status Anzeige
|
||||||
|
const statusIndicator = document.createElement('div');
|
||||||
|
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300';
|
||||||
|
document.body.appendChild(statusIndicator);
|
||||||
|
|
||||||
|
// Zeige Speicherstatus
|
||||||
|
function showStatus(message, isError = false) {
|
||||||
|
statusIndicator.textContent = message;
|
||||||
|
statusIndicator.className = `fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 ${
|
||||||
|
isError
|
||||||
|
? 'bg-red-500 text-white'
|
||||||
|
: 'bg-green-500 text-white'
|
||||||
|
}`;
|
||||||
|
setTimeout(() => {
|
||||||
|
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 opacity-0';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Speicherstatus
|
||||||
|
document.addEventListener('mindmapSaved', () => {
|
||||||
|
showStatus('Änderungen gespeichert');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mindmapError', (event) => {
|
||||||
|
showStatus(event.detail.message, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -39,6 +39,35 @@
|
|||||||
animation: textReveal 1s cubic-bezier(0.77, 0, 0.18, 1) forwards;
|
animation: textReveal 1s cubic-bezier(0.77, 0, 0.18, 1) forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Marker-Animation für den Text */
|
||||||
|
@keyframes markerAnimation {
|
||||||
|
0% { width: 0; opacity: 0; }
|
||||||
|
20% { width: 100%; opacity: 0.7; }
|
||||||
|
80% { width: 100%; opacity: 0.7; }
|
||||||
|
100% { width: 0; opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-animation {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-animation::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 6px;
|
||||||
|
width: 0;
|
||||||
|
background: linear-gradient(to right, rgba(109, 40, 217, 0.3), rgba(139, 92, 246, 0.6), rgba(109, 40, 217, 0.3));
|
||||||
|
border-radius: 3px;
|
||||||
|
opacity: 0;
|
||||||
|
animation: markerAnimation 2.5s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-animation-delay::after {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
.delay-1 { animation-delay: 0.2s; }
|
.delay-1 { animation-delay: 0.2s; }
|
||||||
.delay-2 { animation-delay: 0.4s; }
|
.delay-2 { animation-delay: 0.4s; }
|
||||||
.delay-3 { animation-delay: 0.6s; }
|
.delay-3 { animation-delay: 0.6s; }
|
||||||
@@ -71,16 +100,20 @@
|
|||||||
|
|
||||||
/* Chat section styles */
|
/* Chat section styles */
|
||||||
.embedded-chat {
|
.embedded-chat {
|
||||||
height: 350px;
|
height: 500px;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 5px 10px -5px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .embedded-chat {
|
.dark .embedded-chat {
|
||||||
background-color: rgba(17, 24, 39, 0.7);
|
background-color: rgba(17, 24, 39, 0.7);
|
||||||
border-color: rgba(109, 40, 217, 0.2);
|
border-color: rgba(109, 40, 217, 0.2);
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 5px 10px -5px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.embedded-chat {
|
.embedded-chat {
|
||||||
@@ -89,9 +122,118 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#embedded-chat-messages {
|
#embedded-chat-messages {
|
||||||
height: 250px;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
padding: 1.25rem;
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-container {
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-top: 1px solid;
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .chat-input-container {
|
||||||
|
background-color: rgba(17, 24, 39, 0.6);
|
||||||
|
border-color: rgba(75, 85, 99, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-input {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
border: 1px solid rgba(209, 213, 219, 0.5);
|
||||||
|
color: #4B5563;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mystical-input {
|
||||||
|
background-color: rgba(31, 41, 55, 0.7);
|
||||||
|
border-color: rgba(75, 85, 99, 0.4);
|
||||||
|
color: #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mystical-input:focus {
|
||||||
|
border-color: rgba(139, 92, 246, 0.5);
|
||||||
|
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mystical-input:focus {
|
||||||
|
border-color: rgba(139, 92, 246, 0.5);
|
||||||
|
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Lesbarkeit für Chat-Nachrichten */
|
||||||
|
.chat-message {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
max-width: 85%;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-bubble {
|
||||||
|
background-color: rgba(243, 244, 246, 0.95);
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .assistant-bubble {
|
||||||
|
background-color: rgba(31, 41, 55, 0.95);
|
||||||
|
color: #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-bubble {
|
||||||
|
background-color: rgba(139, 92, 246, 0.15);
|
||||||
|
color: #4B5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .user-bubble {
|
||||||
|
background-color: rgba(124, 58, 237, 0.3);
|
||||||
|
color: #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Beispiel-Buttons verbessert */
|
||||||
|
.quick-query-container {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-query-btn {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
background-color: rgba(243, 244, 246, 0.8);
|
||||||
|
color: #4B5563;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid rgba(209, 213, 219, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .quick-query-btn {
|
||||||
|
background-color: rgba(55, 65, 81, 0.8);
|
||||||
|
color: #E5E7EB;
|
||||||
|
border-color: rgba(75, 85, 99, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-query-btn:hover {
|
||||||
|
background-color: rgba(229, 231, 235, 0.9);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .quick-query-btn:hover {
|
||||||
|
background-color: rgba(75, 85, 99, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat typing indicator */
|
/* Chat typing indicator */
|
||||||
@@ -131,16 +273,9 @@
|
|||||||
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden flex justify-center gap-6">
|
||||||
<span class="gradient-text inline-block text-reveal">Wissen</span>
|
<span class="relative inline-block text-reveal marker-animation">Wissen.</span>
|
||||||
</div>
|
<span class="relative inline-block text-reveal delay-1 marker-animation marker-animation-delay">Vernetzen.</span>
|
||||||
<div class="overflow-hidden mt-2">
|
|
||||||
<span class="inline-block text-reveal delay-1">neu</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 relative overflow-hidden">
|
|
||||||
<span class="relative inline-block text-reveal delay-2">vernetzen
|
|
||||||
<div class="absolute -bottom-2 left-0 right-0 h-1 bg-gradient-to-r from-purple-500/0 via-purple-500/70 to-purple-500/0 rounded-full"></div>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
@@ -179,6 +314,12 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
||||||
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
|
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
|
||||||
|
<!-- Animierte Pfeilspitze -->
|
||||||
|
<div class="mt-6 flex justify-center">
|
||||||
|
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" xmlns="http://www.w3.org/2000/svg" class="text-white animate-bounce-slow">
|
||||||
|
<path d="M10 12L0 2L2 0L10 8L18 0L20 2L10 12Z" fill="currentColor" fill-opacity="0.7"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,14 +412,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chat Messages -->
|
<!-- Chat Messages -->
|
||||||
<div id="embedded-chat-messages" class="border-b border-gray-200 dark:border-gray-700">
|
<div id="embedded-chat-messages" class="border-b-0">
|
||||||
<!-- Assistant Message -->
|
<!-- Assistant Message -->
|
||||||
<div class="mb-4 flex">
|
<div class="chat-message flex">
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
|
<div class="w-9 h-9 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-3 flex-shrink-0">
|
||||||
<i class="fa-solid fa-robot text-sm"></i>
|
<i class="fa-solid fa-robot text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
<div class="chat-bubble assistant-bubble">
|
||||||
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
<div class="markdown-content">
|
||||||
<p>Hallo! Ich bin dein Systades-Assistent. Wie kann ich dir heute helfen?</p>
|
<p>Hallo! Ich bin dein Systades-Assistent. Wie kann ich dir heute helfen?</p>
|
||||||
<p>Du kannst mir Fragen zu:</p>
|
<p>Du kannst mir Fragen zu:</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -292,24 +433,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Message -->
|
<!-- User Message -->
|
||||||
<div class="mb-4 flex justify-end">
|
<div class="chat-message flex justify-end">
|
||||||
<div class="bg-purple-100 dark:bg-purple-900/30 rounded-lg p-3 max-w-[80%]">
|
<div class="chat-bubble user-bubble">
|
||||||
<p class="text-gray-800 dark:text-gray-200">
|
<p>
|
||||||
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
|
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-300 ml-2 flex-shrink-0">
|
<div class="w-9 h-9 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-300 ml-3 flex-shrink-0">
|
||||||
<i class="fa-solid fa-user text-sm"></i>
|
<i class="fa-solid fa-user text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Assistant Response -->
|
<!-- Assistant Response -->
|
||||||
<div class="mb-4 flex" id="demo-ai-response">
|
<div class="chat-message flex" id="demo-ai-response">
|
||||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
|
<div class="w-9 h-9 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-3 flex-shrink-0">
|
||||||
<i class="fa-solid fa-robot text-sm"></i>
|
<i class="fa-solid fa-robot text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
<div class="chat-bubble assistant-bubble">
|
||||||
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
<div class="markdown-content">
|
||||||
<p>Ja, natürlich! Ich kann dir dabei helfen, eine Mindmap zum Thema <strong>Künstliche Intelligenz</strong> zu erstellen.</p>
|
<p>Ja, natürlich! Ich kann dir dabei helfen, eine Mindmap zum Thema <strong>Künstliche Intelligenz</strong> zu erstellen.</p>
|
||||||
<p>Du kannst wie folgt vorgehen:</p>
|
<p>Du kannst wie folgt vorgehen:</p>
|
||||||
<ol>
|
<ol>
|
||||||
@@ -325,19 +466,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chat Input -->
|
<!-- Chat Input -->
|
||||||
<div class="p-4">
|
<div class="chat-input-container">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<input type="text" placeholder="Stelle eine Frage..." class="mystical-input flex-grow" disabled>
|
<input type="text" placeholder="Stelle eine Frage..." class="mystical-input flex-grow" disabled>
|
||||||
<button class="ml-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white p-2 rounded-lg disabled:opacity-50" disabled>
|
<button class="ml-3 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-3 py-2 rounded-lg disabled:opacity-50 flex-shrink-0 hover:shadow-md transition-all duration-200" disabled>
|
||||||
<i class="fa-solid fa-paper-plane"></i>
|
<i class="fa-solid fa-paper-plane"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Quick Queries -->
|
<!-- Quick Queries -->
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
<div class="quick-query-container">
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
|
<span class="text-sm text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
|
||||||
<button data-question="Was sind die wichtigsten Grundlagen der Künstlichen Intelligenz?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">KI-Grundlagen</button>
|
<button data-question="Was sind die wichtigsten Grundlagen der Künstlichen Intelligenz?" class="quick-query-btn hover:shadow-sm">KI-Grundlagen</button>
|
||||||
<button data-question="Wie kann ich eine Mindmap zum Thema Neuronale Netzwerke erstellen?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Mindmap erstellen</button>
|
<button data-question="Wie kann ich eine Mindmap zum Thema Neuronale Netzwerke erstellen?" class="quick-query-btn hover:shadow-sm">Mindmap erstellen</button>
|
||||||
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Datenbank durchsuchen</button>
|
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn hover:shadow-sm">Datenbank durchsuchen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,234 +1,357 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="de">
|
|
||||||
<head>
|
{% block title %}Mindmap{% endblock %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
{% block extra_css %}
|
||||||
<title>Interaktive Mindmap</title>
|
<style>
|
||||||
|
/* Spezifische Stile für die Mindmap-Seite */
|
||||||
|
#cy {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Cytoscape.js -->
|
.mindmap-container {
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Socket.IO -->
|
.dark .mindmap-container {
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Feather Icons (optional) -->
|
.mindmap-toolbar {
|
||||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
<style>
|
body:not(.dark) .mindmap-toolbar {
|
||||||
* {
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
box-sizing: border-box;
|
}
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
.btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-filter {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-filter:not(.active) {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-filter:hover:not(.active) {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kontextmenü */
|
||||||
|
#context-menu {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark #context-menu {
|
||||||
|
background-color: #232837;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) #context-menu {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#context-menu .menu-item {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark #context-menu .menu-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark) #context-menu .menu-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info-Panel */
|
||||||
|
.mindmap-info-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
top: 5rem;
|
||||||
|
width: 280px;
|
||||||
|
background-color: rgba(15, 23, 42, 0.85);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
color: #f1f5f9;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-info-panel.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel-description {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-navigation-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-link {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-link:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mindmap-Toolbar-Buttons */
|
||||||
|
.mindmap-action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: rgba(124, 58, 237, 0.1);
|
||||||
|
color: #8b5cf6;
|
||||||
|
border: 1px solid rgba(124, 58, 237, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .mindmap-action-btn {
|
||||||
|
background-color: rgba(124, 58, 237, 0.15);
|
||||||
|
border-color: rgba(124, 58, 237, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-action-btn:hover {
|
||||||
|
background-color: rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zusätzliches Layout */
|
||||||
|
.mindmap-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.mindmap-section {
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
body {
|
</style>
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
{% endblock %}
|
||||||
background-color: #f9fafb;
|
|
||||||
color: #111827;
|
{% block content %}
|
||||||
line-height: 1.5;
|
<div class="container mx-auto px-4 py-8">
|
||||||
}
|
<div class="flex flex-col lg:flex-row gap-8">
|
||||||
|
<!-- Hauptinhalt -->
|
||||||
.container {
|
<div class="w-full lg:w-3/4">
|
||||||
display: flex;
|
<!-- Mindmap-Titelbereich -->
|
||||||
flex-direction: column;
|
<div class="mb-6">
|
||||||
height: 100vh;
|
<h1 class="text-3xl font-bold mb-2 mystical-glow"
|
||||||
width: 100%;
|
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||||
}
|
Wissenslandkarte
|
||||||
|
</h1>
|
||||||
.header {
|
<p class="opacity-80 text-lg"
|
||||||
background-color: #1f2937;
|
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
color: white;
|
Visualisiere die Verbindungen zwischen Gedanken und Konzepten
|
||||||
padding: 1rem;
|
</p>
|
||||||
display: flex;
|
</div>
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
<!-- Mindmap-Container -->
|
||||||
}
|
<div class="mindmap-container">
|
||||||
|
<!-- Toolbar -->
|
||||||
.header h1 {
|
<div class="mindmap-toolbar">
|
||||||
font-size: 1.5rem;
|
<button id="fit-btn" class="mindmap-action-btn">
|
||||||
font-weight: 500;
|
<i class="fa-solid fa-expand"></i>
|
||||||
}
|
<span>Ansicht anpassen</span>
|
||||||
|
</button>
|
||||||
.toolbar {
|
<button id="reset-btn" class="mindmap-action-btn">
|
||||||
background-color: #f3f4f6;
|
<i class="fa-solid fa-undo"></i>
|
||||||
padding: 0.75rem;
|
<span>Zurücksetzen</span>
|
||||||
display: flex;
|
</button>
|
||||||
gap: 0.5rem;
|
<button id="toggle-labels-btn" class="mindmap-action-btn">
|
||||||
border-bottom: 1px solid #e5e7eb;
|
<i class="fa-solid fa-tags"></i>
|
||||||
}
|
<span>Labels ein/aus</span>
|
||||||
|
</button>
|
||||||
.btn {
|
</div>
|
||||||
background-color: #3b82f6;
|
|
||||||
color: white;
|
<!-- Hauptvisualisierung -->
|
||||||
border: none;
|
<div id="cy"></div>
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0.5rem 1rem;
|
<!-- Info-Panel -->
|
||||||
font-size: 0.875rem;
|
<div id="node-info-panel" class="mindmap-info-panel">
|
||||||
cursor: pointer;
|
<h4 class="info-panel-title">Knoteninfo</h4>
|
||||||
transition: background-color 0.2s;
|
<p id="node-description" class="info-panel-description">Wählen Sie einen Knoten aus...</p>
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
<div class="node-navigation">
|
||||||
gap: 0.5rem;
|
<h5 class="node-navigation-title">Verknüpfte Knoten</h5>
|
||||||
}
|
<div id="connected-nodes" class="node-links">
|
||||||
|
<!-- Wird dynamisch befüllt -->
|
||||||
.btn:hover {
|
</div>
|
||||||
background-color: #2563eb;
|
</div>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background-color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background-color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background-color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cy {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-filter {
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-filter:not(.active) {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-filter:hover:not(.active) {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
padding: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #6b7280;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Kontextmenü Styling */
|
|
||||||
#context-menu {
|
|
||||||
position: absolute;
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#context-menu .menu-item {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#context-menu .menu-item:hover {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header class="header">
|
|
||||||
<h1>Interaktive Mindmap</h1>
|
|
||||||
<div class="search-container">
|
|
||||||
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
<button id="addNode" class="btn">
|
|
||||||
<i data-feather="plus-circle"></i>
|
|
||||||
Knoten hinzufügen
|
|
||||||
</button>
|
|
||||||
<button id="addEdge" class="btn">
|
|
||||||
<i data-feather="git-branch"></i>
|
|
||||||
Verbindung erstellen
|
|
||||||
</button>
|
|
||||||
<button id="editNode" class="btn btn-secondary">
|
|
||||||
<i data-feather="edit-2"></i>
|
|
||||||
Knoten bearbeiten
|
|
||||||
</button>
|
|
||||||
<button id="deleteNode" class="btn btn-danger">
|
|
||||||
<i data-feather="trash-2"></i>
|
|
||||||
Knoten löschen
|
|
||||||
</button>
|
|
||||||
<button id="deleteEdge" class="btn btn-danger">
|
|
||||||
<i data-feather="scissors"></i>
|
|
||||||
Verbindung löschen
|
|
||||||
</button>
|
|
||||||
<button id="reLayout" class="btn btn-secondary">
|
|
||||||
<i data-feather="refresh-cw"></i>
|
|
||||||
Layout neu anordnen
|
|
||||||
</button>
|
|
||||||
<button id="exportMindmap" class="btn btn-secondary">
|
|
||||||
<i data-feather="download"></i>
|
|
||||||
Exportieren
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="category-filters" class="category-filters">
|
<!-- Seitenleiste -->
|
||||||
<!-- Wird dynamisch befüllt -->
|
<div class="w-full lg:w-1/4 space-y-6">
|
||||||
|
<!-- Nutzlänge -->
|
||||||
|
<div class="p-5 rounded-lg overflow-hidden border transition-colors duration-300"
|
||||||
|
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3"
|
||||||
|
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||||
|
<i class="fa-solid fa-circle-info text-purple-400 mr-2"></i>Über die Mindmap
|
||||||
|
</h3>
|
||||||
|
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
<p class="mb-2">Die interaktive Wissenslandkarte zeigt Verbindungen zwischen verschiedenen Gedanken und Konzepten.</p>
|
||||||
|
<p class="mb-2">Sie können:</p>
|
||||||
|
<ul class="list-disc pl-5 space-y-1 text-sm">
|
||||||
|
<li>Knoten auswählen, um Details zu sehen</li>
|
||||||
|
<li>Zoomen (Mausrad oder Pinch-Geste)</li>
|
||||||
|
<li>Die Karte verschieben (Drag & Drop)</li>
|
||||||
|
<li>Die Toolbar nutzen für weitere Aktionen</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kategorienlegende -->
|
||||||
|
<div class="p-5 rounded-lg overflow-hidden border transition-colors duration-300"
|
||||||
|
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3"
|
||||||
|
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||||
|
<i class="fa-solid fa-palette text-purple-400 mr-2"></i>Kategorien
|
||||||
|
</h3>
|
||||||
|
<div id="category-legend" class="space-y-2 text-sm"
|
||||||
|
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
<!-- Wird dynamisch befüllt -->
|
||||||
|
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-purple-500 mr-2"></span> Philosophie</div>
|
||||||
|
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-green-500 mr-2"></span> Wissenschaft</div>
|
||||||
|
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-orange-500 mr-2"></span> Technologie</div>
|
||||||
|
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-pink-500 mr-2"></span> Künste</div>
|
||||||
|
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-blue-500 mr-2"></span> Psychologie</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Meine Mindmaps -->
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<div class="p-5 rounded-lg overflow-hidden border transition-colors duration-300"
|
||||||
|
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||||
|
<h3 class="text-xl font-semibold mb-3"
|
||||||
|
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||||
|
<i class="fa-solid fa-map text-purple-400 mr-2"></i>Meine Mindmaps
|
||||||
|
</h3>
|
||||||
|
<div class="mb-3">
|
||||||
|
<a href="{{ url_for('create_mindmap') }}" class="w-full inline-block py-2 px-4 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-center text-sm font-medium transition-colors">
|
||||||
|
<i class="fa-solid fa-plus mr-1"></i> Neue Mindmap erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 max-h-60 overflow-y-auto"
|
||||||
|
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
{% if user_mindmaps %}
|
||||||
|
{% for mindmap in user_mindmaps %}
|
||||||
|
<a href="{{ url_for('user_mindmap', mindmap_id=mindmap.id) }}" class="block p-2 hover:bg-purple-500/20 rounded-lg transition-colors">
|
||||||
|
<div class="text-sm font-medium">{{ mindmap.name }}</div>
|
||||||
|
<div class="text-xs opacity-70">{{ mindmap.nodes|length }} Knoten</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-sm italic">Sie haben noch keine eigenen Mindmaps erstellt.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cy"></div>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
Mindmap-Anwendung © 2023
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Unsere Mindmap JS -->
|
{% endblock %}
|
||||||
<script src="{{ url_for('static', filename='js/mindmap.js') }}"></script>
|
|
||||||
|
{% block extra_js %}
|
||||||
<!-- Icons initialisieren -->
|
<!-- Cytoscape.js laden -->
|
||||||
<script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.24.0/cytoscape.min.js" integrity="sha512-Ck7jF/eLOvDZ9TpQtO5N0I45/yGNpFKQnHMKVXPQDmQKo4HnWWfGDV0JIeG+kqoGA0TOYCpPNnGQ1gusYv4PA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (typeof feather !== 'undefined') {
|
<!-- Mindmap-Initialisierer laden -->
|
||||||
feather.replace();
|
<script src="/static/js/mindmap-init.js"></script>
|
||||||
}
|
|
||||||
});
|
<script>
|
||||||
</script>
|
// Sobald die Seite und die Scripte geladen sind, initialisiere die Mindmap
|
||||||
</body>
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
</html>
|
// Die Initialisierung wird jetzt direkt in mindmap-init.js ausgeführt
|
||||||
|
console.log('Mindmap-Seite geladen');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -4,17 +4,53 @@
|
|||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
|
/* Gradient Text Styles */
|
||||||
|
.text-gradient-purple-blue {
|
||||||
|
background: linear-gradient(135deg, #b38fff, #58a9ff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient-blue-cyan {
|
||||||
|
background: linear-gradient(135deg, #58a9ff, #38bdf8);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient-cyan-teal {
|
||||||
|
background: linear-gradient(135deg, #38bdf8, #14b8a6);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient-teal-green {
|
||||||
|
background: linear-gradient(135deg, #14b8a6, #22c55e);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient-green-yellow {
|
||||||
|
background: linear-gradient(135deg, #22c55e, #eab308);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
/* Grundstile für das Profil mit verbessertem Glasmorphismus */
|
/* Grundstile für das Profil mit verbessertem Glasmorphismus */
|
||||||
.profile-container {
|
.profile-container {
|
||||||
background: rgba(24, 28, 45, 0.75);
|
background: rgba(24, 28, 45, 0.75);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35),
|
||||||
|
0 8px 16px rgba(179, 143, 255, 0.1);
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-container:hover {
|
.profile-container:hover {
|
||||||
@@ -189,294 +225,64 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(179, 143, 255, 0.5) rgba(255, 255, 255, 0.05);
|
scrollbar-color: rgba(179, 143, 255, 0.5) rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(24, 28, 45, 0.4);
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tab {
|
.profile-tab {
|
||||||
padding: 1rem 1.5rem;
|
padding: 0.75rem 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
border-bottom: 3px solid transparent;
|
border-bottom: 3px solid transparent;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border-radius: 12px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tab:hover {
|
.profile-tab:hover {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
|
||||||
|
|
||||||
.profile-tab.active {
|
|
||||||
color: #b38fff;
|
|
||||||
border-bottom: 3px solid #b38fff;
|
|
||||||
background: rgba(179, 143, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Aktivitäten und Beiträge */
|
|
||||||
.activity-feed {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-card {
|
|
||||||
background: rgba(32, 36, 55, 0.7);
|
|
||||||
border-radius: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-card:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25), 0 0 15px rgba(179, 143, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-header {
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: rgba(255, 255, 255, 0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-date {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-content {
|
|
||||||
padding: 1.5rem;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
font-size: 1.05rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-footer {
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-reactions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reaction-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border: none;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reaction-button:hover {
|
|
||||||
background: rgba(179, 143, 255, 0.15);
|
|
||||||
color: rgba(255, 255, 255, 0.95);
|
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reaction-button.active {
|
/* Statistik-Elemente - verkleinert */
|
||||||
background: rgba(179, 143, 255, 0.2);
|
.stat-item {
|
||||||
color: #b38fff;
|
padding: 0.75rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-actions {
|
.stat-icon {
|
||||||
display: flex;
|
width: 8px !important;
|
||||||
gap: 0.75rem;
|
height: 8px !important;
|
||||||
|
margin-bottom: 0.3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button {
|
.stat-value {
|
||||||
padding: 0.5rem 0.75rem;
|
font-size: 1.2rem !important;
|
||||||
border-radius: 12px;
|
margin-bottom: 0.2rem !important;
|
||||||
font-size: 0.9rem;
|
|
||||||
background: rgba(179, 143, 255, 0.1);
|
|
||||||
color: #b38fff;
|
|
||||||
border: 1px solid rgba(179, 143, 255, 0.25);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button:hover {
|
.stat-label {
|
||||||
background: rgba(179, 143, 255, 0.2);
|
font-size: 0.7rem !important;
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 0 10px rgba(179, 143, 255, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verbesserte Einstellungskarten */
|
/* Persönliche Mindmap Container */
|
||||||
.settings-card {
|
.personal-mindmap-container {
|
||||||
background: rgba(32, 36, 55, 0.7);
|
|
||||||
border-radius: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-card:hover {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2), 0 0 15px rgba(179, 143, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-card-header {
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: rgba(255, 255, 255, 0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-card-body {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-group {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-input {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem 1rem;
|
height: 400px;
|
||||||
background: rgba(24, 28, 45, 0.6);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border-radius: 1rem;
|
||||||
border-radius: 12px;
|
position: relative;
|
||||||
color: rgba(255, 255, 255, 0.95);
|
overflow: hidden;
|
||||||
font-size: 1rem;
|
margin-bottom: 2rem;
|
||||||
transition: all 0.3s ease;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-input:focus {
|
.dark .personal-mindmap-container {
|
||||||
border-color: rgba(179, 143, 255, 0.4);
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
box-shadow: 0 0 0 3px rgba(179, 143, 255, 0.15);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Light Mode Anpassungen */
|
|
||||||
html.light .profile-container,
|
|
||||||
html.light .profile-tabs,
|
|
||||||
html.light .activity-card,
|
|
||||||
html.light .settings-card {
|
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .avatar-container {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border: 3px solid rgba(126, 63, 242, 0.3);
|
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), 0 0 15px rgba(126, 63, 242, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .user-info h1 {
|
|
||||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .user-bio,
|
|
||||||
html.light .activity-content {
|
|
||||||
color: #1a202c;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .user-meta span {
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .stat-item,
|
|
||||||
html.light .settings-input {
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .stat-value {
|
|
||||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .stat-label {
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .profile-tab {
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .profile-tab:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.03);
|
|
||||||
color: #1a202c;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .profile-tab.active {
|
|
||||||
color: #7e3ff2;
|
|
||||||
border-bottom: 3px solid #7e3ff2;
|
|
||||||
background: rgba(126, 63, 242, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .activity-title,
|
|
||||||
html.light .settings-card-header,
|
|
||||||
html.light .settings-label {
|
|
||||||
color: #1a202c;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .activity-date {
|
|
||||||
color: #718096;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .activity-footer {
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .reaction-button {
|
|
||||||
color: #4a5568;
|
|
||||||
background: rgba(0, 0, 0, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .reaction-button:hover {
|
|
||||||
background: rgba(126, 63, 242, 0.1);
|
|
||||||
color: #7e3ff2;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .reaction-button.active {
|
|
||||||
background: rgba(126, 63, 242, 0.15);
|
|
||||||
color: #7e3ff2;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .action-button {
|
|
||||||
background: rgba(126, 63, 242, 0.1);
|
|
||||||
color: #7e3ff2;
|
|
||||||
border: 1px solid rgba(126, 63, 242, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light .action-button:hover {
|
|
||||||
background: rgba(126, 63, 242, 0.2);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -488,7 +294,23 @@
|
|||||||
<!-- User Info Section -->
|
<!-- User Info Section -->
|
||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<img src="{{ user.avatar if user.avatar else url_for('static', filename='img/default-avatar.png') }}" alt="Profilbild" class="avatar">
|
{% if user.avatar %}
|
||||||
|
<img src="{{ user.avatar }}" alt="Profilbild" class="avatar">
|
||||||
|
{% else %}
|
||||||
|
<div class="avatar default-avatar">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="100" cy="100" r="98" fill="url(#gradient)" stroke="#7C3AED" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="80" r="36" fill="white"/>
|
||||||
|
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#8B5CF6"/>
|
||||||
|
<stop offset="1" stop-color="#3B82F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="avatar-edit">
|
<div class="avatar-edit">
|
||||||
<i class="fas fa-camera"></i>
|
<i class="fas fa-camera"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -505,50 +327,60 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistiken -->
|
<!-- Statistiken -->
|
||||||
<div class="profile-stats">
|
<div class="profile-stats grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mt-6">
|
||||||
<!-- Gedanken -->
|
<!-- Gedanken -->
|
||||||
<div class="stat-item">
|
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-purple-500/20 to-blue-500/20 mb-3">
|
||||||
<i class="fas fa-lightbulb"></i>
|
<i class="fas fa-lightbulb text-xl text-gradient-purple-blue"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ stats.thought_count if stats and stats.thought_count else 0 }}</div>
|
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">
|
||||||
<div class="stat-label">Gedanken</div>
|
{{ stats.thought_count if stats and stats.thought_count else 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label text-sm text-gray-400">Gedanken</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Verbindungen -->
|
<!-- Verbindungen -->
|
||||||
<div class="stat-item">
|
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-blue-500/20 to-cyan-500/20 mb-3">
|
||||||
<i class="fas fa-project-diagram"></i>
|
<i class="fas fa-project-diagram text-xl text-gradient-blue-cyan"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ stats.connections_count if stats and stats.connections_count else 0 }}</div>
|
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
|
||||||
<div class="stat-label">Verbindungen</div>
|
{{ stats.connections_count if stats and stats.connections_count else 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label text-sm text-gray-400">Verbindungen</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Follower -->
|
<!-- Follower -->
|
||||||
<div class="stat-item">
|
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-cyan-500/20 to-teal-500/20 mb-3">
|
||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-users text-xl text-gradient-cyan-teal"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ stats.followers_count if stats and stats.followers_count else 0 }}</div>
|
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-cyan-400 to-teal-400 bg-clip-text text-transparent">
|
||||||
<div class="stat-label">Follower</div>
|
{{ stats.followers_count if stats and stats.followers_count else 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label text-sm text-gray-400">Follower</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Beiträge -->
|
<!-- Beiträge -->
|
||||||
<div class="stat-item">
|
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-teal-500/20 to-green-500/20 mb-3">
|
||||||
<i class="fas fa-comment-dots"></i>
|
<i class="fas fa-comment-dots text-xl text-gradient-teal-green"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ stats.contributions_count if stats and stats.contributions_count else 0 }}</div>
|
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text text-transparent">
|
||||||
<div class="stat-label">Beiträge</div>
|
{{ stats.contributions_count if stats and stats.contributions_count else 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label text-sm text-gray-400">Beiträge</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bewertung -->
|
<!-- Bewertung -->
|
||||||
<div class="stat-item">
|
<div class="stat-item bg-opacity-70 backdrop-blur-md rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 hover:transform hover:scale-105">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon w-12 h-12 rounded-full flex items-center justify-center bg-gradient-to-br from-green-500/20 to-yellow-500/20 mb-3">
|
||||||
<i class="fas fa-star"></i>
|
<i class="fas fa-star text-xl text-gradient-green-yellow"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ stats.rating if stats and stats.rating else '0.0' }}</div>
|
<div class="stat-value text-2xl font-bold mb-1 bg-gradient-to-r from-green-400 to-yellow-400 bg-clip-text text-transparent">
|
||||||
<div class="stat-label">Bewertung</div>
|
{{ stats.rating if stats and stats.rating else '0.0' }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label text-sm text-gray-400">Bewertung</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -559,6 +391,7 @@
|
|||||||
<div class="profile-tabs">
|
<div class="profile-tabs">
|
||||||
<div class="profile-tab active" data-tab="activity">Aktivitäten</div>
|
<div class="profile-tab active" data-tab="activity">Aktivitäten</div>
|
||||||
<div class="profile-tab" data-tab="thoughts">Gedanken</div>
|
<div class="profile-tab" data-tab="thoughts">Gedanken</div>
|
||||||
|
<div class="profile-tab" data-tab="mindmaps">Mindmaps</div>
|
||||||
<div class="profile-tab" data-tab="collections">Sammlungen</div>
|
<div class="profile-tab" data-tab="collections">Sammlungen</div>
|
||||||
<div class="profile-tab" data-tab="connections">Verbindungen</div>
|
<div class="profile-tab" data-tab="connections">Verbindungen</div>
|
||||||
<div class="profile-tab" data-tab="settings">Einstellungen</div>
|
<div class="profile-tab" data-tab="settings">Einstellungen</div>
|
||||||
@@ -612,21 +445,87 @@
|
|||||||
<div class="tab-content hidden" id="thoughts-tab">
|
<div class="tab-content hidden" id="thoughts-tab">
|
||||||
<div id="thoughts-container">
|
<div id="thoughts-container">
|
||||||
{% if thoughts %}
|
{% if thoughts %}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{% for thought in thoughts %}
|
{% for thought in thoughts %}
|
||||||
<div class="thought-item">
|
<div class="thought-item bg-opacity-70 rounded-xl overflow-hidden border transition-all duration-300 hover:transform hover:scale-105 hover:shadow-lg"
|
||||||
<h3>{{ thought.title }}</h3>
|
x-bind:class="darkMode ? 'bg-gray-800/80 border-gray-700/60' : 'bg-white/90 border-gray-200/60'">
|
||||||
<p>{{ thought.content }}</p>
|
<div class="p-5 thought-border" data-color="{{ thought.color_code|default('#B39DDB') }}">
|
||||||
<div class="thought-meta">
|
<h3 class="text-xl font-bold mb-2"
|
||||||
<span>{{ thought.date }}</span>
|
x-bind:class="darkMode ? 'text-purple-300' : 'text-purple-700'">{{ thought.title }}</h3>
|
||||||
<span>{{ thought.category }}</span>
|
<p class="mb-4 text-sm"
|
||||||
|
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
{{ thought.abstract or thought.content[:150] ~ '...' }}
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-between items-center text-xs"
|
||||||
|
x-bind:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
|
||||||
|
<span>{{ thought.created_at.strftime('%d.%m.%Y') }}</span>
|
||||||
|
<span>{{ thought.branch }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 border-t flex justify-between items-center"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-900/80 border-gray-700/60' : 'bg-gray-50/80 border-gray-200/60'">
|
||||||
|
<a href="{{ url_for('get_thought', thought_id=thought.id) }}" class="transition-colors"
|
||||||
|
x-bind:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||||
|
<i class="fas fa-eye mr-1"></i> Ansehen
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('update_thought', thought_id=thought.id) }}" class="transition-colors"
|
||||||
|
x-bind:class="darkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-500'">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
||||||
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
||||||
<a href="{{ url_for('get_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a>
|
<a href="{{ url_for('add_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">Ersten Gedanken erstellen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mindmaps-Tab -->
|
||||||
|
<div class="tab-content hidden" id="mindmaps-tab">
|
||||||
|
<div id="mindmaps-container">
|
||||||
|
{% if user_mindmaps %}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{% for mindmap in user_mindmaps %}
|
||||||
|
<div class="mindmap-item bg-opacity-70 rounded-xl overflow-hidden border transition-all duration-300 hover:transform hover:scale-105 hover:shadow-lg"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-800/80 border-gray-700/60' : 'bg-white/90 border-gray-200/60'">
|
||||||
|
<div class="p-5">
|
||||||
|
<h3 class="text-xl font-bold mb-2"
|
||||||
|
x-bind:class="darkMode ? 'text-purple-400' : 'text-purple-700'">{{ mindmap.name }}</h3>
|
||||||
|
<p class="mb-4 text-sm"
|
||||||
|
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">{{ mindmap.description }}</p>
|
||||||
|
<div class="flex justify-between items-center text-xs"
|
||||||
|
x-bind:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
|
||||||
|
<span>Erstellt: {{ mindmap.created_at.strftime('%d.%m.%Y') }}</span>
|
||||||
|
<span>Zuletzt bearbeitet: {{ mindmap.last_modified.strftime('%d.%m.%Y') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 border-t flex justify-between"
|
||||||
|
x-bind:class="darkMode ? 'bg-gray-900/80 border-gray-700/60' : 'bg-gray-50/80 border-gray-200/60'">
|
||||||
|
<a href="{{ url_for('mindmap') }}?id={{ mindmap.id }}" class="transition-colors"
|
||||||
|
x-bind:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||||
|
<i class="fas fa-eye mr-1"></i> Anzeigen
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="transition-colors"
|
||||||
|
x-bind:class="darkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-500'">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<i class="fas fa-project-diagram text-5xl text-gray-400 mb-4"></i>
|
||||||
|
<p class="text-gray-500">Noch keine Mindmaps erstellt</p>
|
||||||
|
<a href="{{ url_for('create_mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
|
||||||
|
Erste Mindmap erstellen
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -649,7 +548,7 @@
|
|||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<i class="fas fa-folder-open text-5xl text-gray-400 mb-4"></i>
|
<i class="fas fa-folder-open text-5xl text-gray-400 mb-4"></i>
|
||||||
<p class="text-gray-500">Noch keine Sammlungen erstellt</p>
|
<p class="text-gray-500">Noch keine Sammlungen erstellt</p>
|
||||||
<a href="{{ url_for('create_collection') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Erste Sammlung erstellen</a>
|
<a href="{{ url_for('profile') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">Zurück zum Profil</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -683,7 +582,7 @@
|
|||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<i class="fas fa-project-diagram text-5xl text-gray-400 mb-4"></i>
|
<i class="fas fa-project-diagram text-5xl text-gray-400 mb-4"></i>
|
||||||
<p class="text-gray-500">Noch keine Verbindungen erstellt</p>
|
<p class="text-gray-500">Noch keine Verbindungen erstellt</p>
|
||||||
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Verbindungen in der Mindmap erstellen</a>
|
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">Verbindungen in der Mindmap erstellen</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -769,7 +668,21 @@
|
|||||||
|
|
||||||
// Entsprechenden Tab-Inhalt anzeigen
|
// Entsprechenden Tab-Inhalt anzeigen
|
||||||
const tabId = this.getAttribute('data-tab');
|
const tabId = this.getAttribute('data-tab');
|
||||||
document.getElementById(`${tabId}-tab`).classList.remove('hidden');
|
const tabContent = document.getElementById(`${tabId}-tab`);
|
||||||
|
|
||||||
|
if (tabContent) {
|
||||||
|
tabContent.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Animation für Tab-Inhalt
|
||||||
|
tabContent.style.opacity = '0';
|
||||||
|
tabContent.style.transform = 'translateY(10px)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
tabContent.style.transition = 'all 0.3s ease';
|
||||||
|
tabContent.style.opacity = '1';
|
||||||
|
tabContent.style.transform = 'translateY(0)';
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -795,6 +708,280 @@
|
|||||||
countElement.textContent = count;
|
countElement.textContent = count;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Profilbearbeitung
|
||||||
|
const editProfileBtn = document.querySelector('.edit-profile-btn');
|
||||||
|
|
||||||
|
if (editProfileBtn) {
|
||||||
|
editProfileBtn.addEventListener('click', function() {
|
||||||
|
// Zum Einstellungstab wechseln
|
||||||
|
const settingsTab = document.querySelector('[data-tab="settings"]');
|
||||||
|
if (settingsTab) {
|
||||||
|
settingsTab.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avatar-Bearbeitung
|
||||||
|
const avatarEditBtn = document.querySelector('.avatar-edit');
|
||||||
|
if (avatarEditBtn) {
|
||||||
|
avatarEditBtn.addEventListener('click', function() {
|
||||||
|
// Dateiauwahl öffnen
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = 'image/*';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
|
||||||
|
fileInput.click();
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', function() {
|
||||||
|
if (this.files && this.files[0]) {
|
||||||
|
// Anzeigen des gewählten Bildes
|
||||||
|
const avatarImg = document.querySelector('.avatar');
|
||||||
|
|
||||||
|
// FileReader zum Einlesen des Bildes
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
// Vorschau anzeigen
|
||||||
|
avatarImg.src = e.target.result;
|
||||||
|
|
||||||
|
// Avatar-URL im Einstellungsbereich speichern
|
||||||
|
const avatarUrlInput = document.createElement('input');
|
||||||
|
avatarUrlInput.type = 'hidden';
|
||||||
|
avatarUrlInput.name = 'avatar_url';
|
||||||
|
avatarUrlInput.id = 'avatar_url';
|
||||||
|
avatarUrlInput.value = e.target.result;
|
||||||
|
|
||||||
|
// Entferne vorhandenes Input, falls vorhanden
|
||||||
|
const existingInput = document.getElementById('avatar_url');
|
||||||
|
if (existingInput) {
|
||||||
|
existingInput.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zum Formular hinzufügen
|
||||||
|
const settingsForm = document.querySelector('.settings-card');
|
||||||
|
if (settingsForm) {
|
||||||
|
settingsForm.appendChild(avatarUrlInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erfolgsmeldung anzeigen
|
||||||
|
showNotification('Avatar wurde aktualisiert! Bitte speichere deine Änderungen.', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(this.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input entfernen
|
||||||
|
document.body.removeChild(fileInput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einstellungen-Formular-Handling
|
||||||
|
const saveSettingsBtn = document.querySelectorAll('.settings-card .profile-action-btn.primary');
|
||||||
|
|
||||||
|
saveSettingsBtn.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const isPasswordUpdate = this.textContent.includes('Passwort');
|
||||||
|
|
||||||
|
// Passwort-Update
|
||||||
|
if (isPasswordUpdate) {
|
||||||
|
const currentPassword = document.getElementById('password').value;
|
||||||
|
const newPassword = document.getElementById('password_confirm').value;
|
||||||
|
|
||||||
|
if (!currentPassword || !newPassword) {
|
||||||
|
showNotification('Bitte fülle alle Passwortfelder aus', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AJAX-Anfrage senden
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'update_password');
|
||||||
|
formData.append('current_password', currentPassword);
|
||||||
|
formData.append('new_password', newPassword);
|
||||||
|
formData.append('confirm_password', newPassword);
|
||||||
|
|
||||||
|
// Visuelle Rückmeldung
|
||||||
|
const originalText = this.innerHTML;
|
||||||
|
this.innerHTML = '<i class="fas fa-circle-notch fa-spin mr-1"></i> Speichern...';
|
||||||
|
this.disabled = true;
|
||||||
|
|
||||||
|
fetch('{{ url_for("settings") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Aktualisieren des Passworts:', error);
|
||||||
|
return { success: false, message: 'Netzwerkfehler. Bitte versuche es erneut.' };
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
this.innerHTML = originalText;
|
||||||
|
this.disabled = false;
|
||||||
|
|
||||||
|
if (data && data.success) {
|
||||||
|
showNotification('Passwort erfolgreich aktualisiert!', 'success');
|
||||||
|
document.getElementById('password').value = '';
|
||||||
|
document.getElementById('password_confirm').value = '';
|
||||||
|
} else {
|
||||||
|
showNotification(data?.message || 'Fehler beim Aktualisieren des Passworts', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Profil-Update
|
||||||
|
else {
|
||||||
|
// Sammle Daten aus den Eingabefeldern
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'update_profile');
|
||||||
|
formData.append('bio', document.getElementById('bio').value || '');
|
||||||
|
formData.append('location', document.getElementById('location').value || '');
|
||||||
|
formData.append('website', document.getElementById('website').value || '');
|
||||||
|
|
||||||
|
// Avatar hinzufügen, falls vorhanden
|
||||||
|
const avatarUrlInput = document.getElementById('avatar_url');
|
||||||
|
if (avatarUrlInput) {
|
||||||
|
formData.append('avatar_url', avatarUrlInput.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visuelle Rückmeldung
|
||||||
|
const originalText = this.innerHTML;
|
||||||
|
this.innerHTML = '<i class="fas fa-circle-notch fa-spin mr-1"></i> Speichern...';
|
||||||
|
this.disabled = true;
|
||||||
|
|
||||||
|
// AJAX-Anfrage senden
|
||||||
|
fetch('{{ url_for("settings") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Speichern');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Speichern der Profileinstellungen:', error);
|
||||||
|
return { success: false, message: 'Netzwerkfehler. Bitte versuche es erneut.' };
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
this.innerHTML = originalText;
|
||||||
|
this.disabled = false;
|
||||||
|
|
||||||
|
if (data && data.success) {
|
||||||
|
// Erfolgsanimation
|
||||||
|
showNotification('Profil erfolgreich aktualisiert!', 'success');
|
||||||
|
|
||||||
|
// UI aktualisieren ohne Neuladen
|
||||||
|
const bioElement = document.querySelector('.user-bio');
|
||||||
|
const locationElement = document.querySelector('.user-meta span:first-child');
|
||||||
|
|
||||||
|
if (bioElement) {
|
||||||
|
bioElement.textContent = document.getElementById('bio').value || 'Keine Bio vorhanden. Klicke auf bearbeiten, um eine hinzuzufügen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locationElement) {
|
||||||
|
const location = document.getElementById('location').value;
|
||||||
|
locationElement.innerHTML = `<i class="fas fa-map-marker-alt"></i> ${location || 'Kein Standort angegeben'}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showNotification(data?.message || 'Fehler beim Aktualisieren des Profils', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gedanken-Karten mit Hover-Effekten und Border-Farben
|
||||||
|
const thoughtItems = document.querySelectorAll('.thought-item');
|
||||||
|
thoughtItems.forEach(item => {
|
||||||
|
// Hover-Effekte
|
||||||
|
item.addEventListener('mouseenter', () => {
|
||||||
|
item.style.transform = 'translateY(-5px)';
|
||||||
|
item.style.boxShadow = '0 12px 30px rgba(0, 0, 0, 0.15)';
|
||||||
|
});
|
||||||
|
|
||||||
|
item.addEventListener('mouseleave', () => {
|
||||||
|
item.style.transform = 'translateY(0)';
|
||||||
|
item.style.boxShadow = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Border-Farben anwenden
|
||||||
|
const borderElem = item.querySelector('.thought-border');
|
||||||
|
if (borderElem && borderElem.dataset.color) {
|
||||||
|
borderElem.style.borderLeftColor = borderElem.dataset.color;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mindmap-Karten mit Hover-Effekten
|
||||||
|
const mindmapItems = document.querySelectorAll('.mindmap-item');
|
||||||
|
mindmapItems.forEach(item => {
|
||||||
|
item.addEventListener('mouseenter', () => {
|
||||||
|
item.style.transform = 'translateY(-5px)';
|
||||||
|
item.style.boxShadow = '0 12px 30px rgba(0, 0, 0, 0.15)';
|
||||||
|
});
|
||||||
|
|
||||||
|
item.addEventListener('mouseleave', () => {
|
||||||
|
item.style.transform = 'translateY(0)';
|
||||||
|
item.style.boxShadow = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Benachrichtigungsfunktion
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
// Bestehende Benachrichtigung entfernen
|
||||||
|
const existingNotification = document.getElementById('notification');
|
||||||
|
if (existingNotification) {
|
||||||
|
existingNotification.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Benachrichtigung erstellen
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.id = 'notification';
|
||||||
|
notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 flex items-center transform transition-all duration-300 translate-y-0 opacity-0`;
|
||||||
|
|
||||||
|
// Typ-basierte Stile
|
||||||
|
if (type === 'success') {
|
||||||
|
notification.classList.add('bg-green-500', 'text-white');
|
||||||
|
notification.innerHTML = `<i class="fas fa-check-circle mr-2"></i> ${message}`;
|
||||||
|
} else if (type === 'error') {
|
||||||
|
notification.classList.add('bg-red-500', 'text-white');
|
||||||
|
notification.innerHTML = `<i class="fas fa-exclamation-circle mr-2"></i> ${message}`;
|
||||||
|
} else {
|
||||||
|
notification.classList.add('bg-blue-500', 'text-white');
|
||||||
|
notification.innerHTML = `<i class="fas fa-info-circle mr-2"></i> ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close-Button
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.className = 'ml-4 text-white opacity-75 hover:opacity-100';
|
||||||
|
closeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
notification.classList.add('opacity-0', 'translate-y-[-10px]');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.appendChild(closeBtn);
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Animation starten
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('opacity-0');
|
||||||
|
notification.classList.add('opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Automatisch ausblenden nach 5 Sekunden
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(notification)) {
|
||||||
|
notification.classList.add('opacity-0', 'translate-y-[-10px]');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(notification)) {
|
||||||
|
notification.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
75
templates/simple_profile.html
Normal file
75
templates/simple_profile.html
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Einfaches Profil{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-10">
|
||||||
|
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
|
||||||
|
<h1 class="text-3xl font-bold text-purple-400 mb-4">Hallo, {{ user.username }}</h1>
|
||||||
|
<div class="text-gray-300 mb-4">
|
||||||
|
<p>E-Mail: {{ user.email }}</p>
|
||||||
|
<p>Mitglied seit: {{ user.created_at.strftime('%d.%m.%Y') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold text-purple-300 mt-6 mb-3">Deine Mindmaps</h2>
|
||||||
|
{% if user_mindmaps %}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{% for mindmap in user_mindmaps %}
|
||||||
|
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
|
||||||
|
<h3 class="text-lg font-medium text-purple-400 mb-2">{{ mindmap.name }}</h3>
|
||||||
|
<p class="text-gray-300 text-sm mb-3">{{ mindmap.description }}</p>
|
||||||
|
<div class="flex justify-between text-xs text-gray-400">
|
||||||
|
<span>Erstellt: {{ mindmap.created_at.strftime('%d.%m.%Y') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-between">
|
||||||
|
<a href="{{ url_for('mindmap') }}?id={{ mindmap.id }}" class="text-purple-400 hover:text-purple-300">
|
||||||
|
<i class="fas fa-eye mr-1"></i> Anzeigen
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="text-blue-400 hover:text-blue-300">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-6">
|
||||||
|
<p class="text-gray-400">Du hast noch keine Mindmaps erstellt</p>
|
||||||
|
<a href="{{ url_for('create_mindmap') }}" class="mt-3 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
|
||||||
|
Erste Mindmap erstellen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold text-purple-300 mt-8 mb-3">Deine Gedanken</h2>
|
||||||
|
{% if thoughts %}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{% for thought in thoughts %}
|
||||||
|
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
|
||||||
|
<h3 class="text-lg font-medium text-purple-400 mb-2">{{ thought.title }}</h3>
|
||||||
|
<p class="text-gray-300 text-sm mb-2">
|
||||||
|
{{ thought.abstract[:150] ~ '...' if thought.abstract and thought.abstract|length > 150 else thought.abstract }}
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-between text-xs text-gray-400">
|
||||||
|
<span>Erstellt: {{ thought.created_at.strftime('%d.%m.%Y') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-6">
|
||||||
|
<p class="text-gray-400">Du hast noch keine Gedanken erstellt</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-8 flex justify-between">
|
||||||
|
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
|
||||||
|
Zurück zur Startseite
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('logout') }}" class="px-4 py-2 bg-red-700 text-white rounded-lg hover:bg-red-600">
|
||||||
|
Abmelden
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
391
templates/user_mindmap.html
Normal file
391
templates/user_mindmap.html
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
{% 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"></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', function() {
|
||||||
|
// Benutzer-Mindmap-ID für die API-Anfragen
|
||||||
|
const mindmapId = {{ mindmap.id }};
|
||||||
|
|
||||||
|
// Erstellt eine neue MindMap-Instanz für die Benutzer-Mindmap
|
||||||
|
window.userMindmap = new MindMap('#cy', {
|
||||||
|
editable: true,
|
||||||
|
isUserLoggedIn: true,
|
||||||
|
isPublicMap: false,
|
||||||
|
userMindmapId: mindmapId,
|
||||||
|
fitViewOnInit: true,
|
||||||
|
callbacks: {
|
||||||
|
onLoad: function() {
|
||||||
|
console.log('Benutzerdefinierte Mindmap wurde geladen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event-Listener für Notiz-Button
|
||||||
|
document.getElementById('add-note-btn').addEventListener('click', function() {
|
||||||
|
// Erstellt eine neue Notiz in der Mitte des Viewports
|
||||||
|
const position = window.userMindmap.cy.pan();
|
||||||
|
|
||||||
|
window.userMindmap.showAddNoteDialog({
|
||||||
|
x: position.x,
|
||||||
|
y: position.y
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event-Listener für Layout-Speichern-Button
|
||||||
|
document.getElementById('save-layout-btn').addEventListener('click', function() {
|
||||||
|
window.userMindmap.saveLayout();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
65
update_db.py
Normal file
65
update_db.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Bestimme den absoluten Pfad zur Datenbank
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
db_path = os.path.join(basedir, 'database', 'systades.db')
|
||||||
|
|
||||||
|
def update_user_table():
|
||||||
|
"""Aktualisiert die User-Tabelle mit den fehlenden Spalten"""
|
||||||
|
|
||||||
|
# Überprüfe, ob die Datenbankdatei existiert
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
print(f"Datenbank nicht gefunden unter: {db_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Verbindung zur Datenbank herstellen
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Überprüfe, ob die neuen Spalten bereits existieren
|
||||||
|
cursor.execute("PRAGMA table_info(user)")
|
||||||
|
columns = [info[1] for info in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Neue Spalten, die hinzugefügt werden müssen
|
||||||
|
new_columns = {
|
||||||
|
'bio': 'TEXT',
|
||||||
|
'location': 'VARCHAR(100)',
|
||||||
|
'website': 'VARCHAR(200)',
|
||||||
|
'avatar': 'VARCHAR(200)',
|
||||||
|
'last_login': 'DATETIME'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Spalten hinzufügen, die noch nicht existieren
|
||||||
|
for col_name, col_type in new_columns.items():
|
||||||
|
if col_name not in columns:
|
||||||
|
print(f"Füge Spalte '{col_name}' zur User-Tabelle hinzu...")
|
||||||
|
cursor.execute(f"ALTER TABLE user ADD COLUMN {col_name} {col_type}")
|
||||||
|
|
||||||
|
# Änderungen speichern
|
||||||
|
conn.commit()
|
||||||
|
print("User-Tabelle erfolgreich aktualisiert!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"Fehler bei der Datenbankaktualisierung: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Führe die Aktualisierung durch
|
||||||
|
success = update_user_table()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("Die Datenbank wurde erfolgreich aktualisiert.")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("Es gab ein Problem bei der Datenbankaktualisierung.")
|
||||||
|
sys.exit(1)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -55,7 +55,7 @@ def create_user(username, email, password, is_admin=False):
|
|||||||
user = User(
|
user = User(
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
is_admin=is_admin,
|
role='admin' if is_admin else 'user',
|
||||||
created_at=datetime.utcnow()
|
created_at=datetime.utcnow()
|
||||||
)
|
)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
home = C:\Program Files\Python313
|
home = C:\Program Files\Python313
|
||||||
include-system-site-packages = false
|
include-system-site-packages = false
|
||||||
version = 3.13.3
|
version = 3.13.3
|
||||||
executable = C:\Program Files\Python313\python.exe
|
executable = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe
|
||||||
command = C:\Program Files\Python313\python.exe -m venv C:\Users\TTOMCZA.EMEA\Dev\website\venv
|
command = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe -m venv C:\Users\firem\Desktop\111\Systades\website\venv
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
@echo off
|
|
||||||
echo Mindmap Projekt - Windows Setup
|
|
||||||
echo ==============================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM Prüfen, ob Python installiert ist
|
|
||||||
python --version >nul 2>&1
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo Python ist nicht installiert oder nicht im PATH.
|
|
||||||
echo Bitte installiere Python 3.11 von https://www.python.org/downloads/
|
|
||||||
echo und stelle sicher, dass "Add Python to PATH" während der Installation aktiviert ist.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Erstelle virtuelle Umgebung...
|
|
||||||
python -m venv venv
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo Fehler beim Erstellen der virtuellen Umgebung.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Aktiviere virtuelle Umgebung...
|
|
||||||
call venv\Scripts\activate.bat
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo Fehler beim Aktivieren der virtuellen Umgebung.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Aktualisiere pip...
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo Warnung: Pip konnte nicht aktualisiert werden. Fahre trotzdem fort.
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Installiere Abhängigkeiten...
|
|
||||||
pip install -r requirements.txt
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo Fehler beim Installieren der Abhängigkeiten.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Setup abgeschlossen!
|
|
||||||
echo.
|
|
||||||
echo Zum Starten des Servers:
|
|
||||||
echo 1. Führe "venv\Scripts\activate.bat" aus
|
|
||||||
echo 2. Führe "python TOOLS.py db:rebuild" aus (Nur beim ersten Mal oder zum Zurücksetzen der Datenbank)
|
|
||||||
echo 3. Führe "python TOOLS.py user:admin" aus (Erstellt einen Admin-Benutzer: admin/admin)
|
|
||||||
echo 4. Führe "python TOOLS.py server:run" aus
|
|
||||||
echo.
|
|
||||||
echo Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
|
||||||
echo.
|
|
||||||
|
|
||||||
pause
|
|
||||||
Reference in New Issue
Block a user