Compare commits
69 Commits
a5396c0d6e
...
tills-bran
| Author | SHA1 | Date | |
|---|---|---|---|
| a7bb9563b3 | |||
| 4e0c470663 | |||
| b5300f74bd | |||
| 6a53e621ca | |||
| a59ce652af | |||
| 27cfc95081 | |||
| c513666391 | |||
| ae30dbce57 | |||
| 817ddd98e9 | |||
| bfce2fc7b7 | |||
| efbcd567ee | |||
| a873765d08 | |||
| efbcadb95a | |||
| da3ccaffe9 | |||
| f4e04573bd | |||
| aa253f3871 | |||
| cfd6a25b21 | |||
| d307763007 | |||
| d7e6912e08 | |||
| ffe96074f4 | |||
| 49ccf3908a | |||
| 9514645904 | |||
| 63f45abb3e | |||
| 7d74b5a7bf | |||
| 55f2553780 | |||
| 0852ea070b | |||
| 7a0533ac09 | |||
| 65c44ab371 | |||
| 5399169b11 | |||
| 05f6f149ad | |||
| 12ed413c04 | |||
| ccf5a1d678 | |||
| eb23d638e6 | |||
| 750dba2d6f | |||
| 6aa3780a96 | |||
| 8890a62026 | |||
| 6cf9b2a627 | |||
| 4f6aea8e20 | |||
| e5f485d9d7 | |||
| cf3fc09a63 | |||
| 10747a8336 | |||
| 7eb958f3c8 | |||
| 4a3092a4d2 | |||
| 2d8cdc052f | |||
| 968515ce2b | |||
| 88f8e98df0 | |||
| e5409eef68 | |||
| 013bf76446 | |||
| 808a3c7bbe | |||
| d117978005 | |||
| 48d8463481 | |||
| 08314ec703 | |||
| 0bb7d8d0dc | |||
| 4a28c2c453 | |||
| 66d987857a | |||
| d58aba26c2 | |||
| 8f0a6d4372 | |||
| 5372fe220e | |||
| 11ab15127c | |||
| 0705ecce59 | |||
| 1c59b0b616 | |||
| d42c43db50 | |||
| e46264b201 | |||
| 74307ba345 | |||
| 14474c4eab | |||
| 4797cc3b72 | |||
| ab280b55af | |||
| 84b492d8d2 | |||
| b0db3398f2 |
17
.env
17
.env
@@ -1,15 +1,2 @@
|
||||
# MindMap Umgebungsvariablen
|
||||
# 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
|
||||
SECRET_KEY=eed9298856dc9363cd32778265780d6904ba24e6a6b815a2cc382bcdd767ea7b
|
||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||
|
||||
33
Dockerfile
33
Dockerfile
@@ -1,33 +0,0 @@
|
||||
# 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.
579
app.py
579
app.py
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta
|
||||
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_sqlalchemy import SQLAlchemy
|
||||
@@ -19,13 +19,12 @@ from openai import OpenAI
|
||||
from dotenv import load_dotenv
|
||||
from flask_socketio import SocketIO, emit
|
||||
from flask_migrate import Migrate
|
||||
import sqlalchemy
|
||||
|
||||
# Modelle importieren
|
||||
from models import (
|
||||
db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating,
|
||||
RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote,
|
||||
node_thought_association, user_thought_bookmark, node_relationship, ForumCategory, ForumPost
|
||||
node_thought_association, user_thought_bookmark, node_relationship
|
||||
)
|
||||
|
||||
# Lade .env-Datei
|
||||
@@ -191,31 +190,6 @@ def create_default_categories():
|
||||
db.session.commit()
|
||||
print("Standard-Kategorien wurden erstellt!")
|
||||
|
||||
def create_forum_categories():
|
||||
"""Erstellt Forum-Kategorien basierend auf Hauptknotenpunkten der Mindmap"""
|
||||
# Hauptknotenpunkte abrufen (nur die, die keine Elternknoten haben)
|
||||
main_nodes = MindMapNode.query.filter(~MindMapNode.id.in_(
|
||||
db.session.query(node_relationship.c.child_id)
|
||||
)).all()
|
||||
|
||||
for node in main_nodes:
|
||||
# Prüfen, ob eine Forum-Kategorie für diesen Knoten bereits existiert
|
||||
existing_category = ForumCategory.query.filter_by(node_id=node.id).first()
|
||||
if existing_category:
|
||||
continue
|
||||
|
||||
# Neue Kategorie erstellen
|
||||
forum_category = ForumCategory(
|
||||
node_id=node.id,
|
||||
title=node.name,
|
||||
description=node.description,
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(forum_category)
|
||||
|
||||
db.session.commit()
|
||||
print("Forum-Kategorien wurden für alle Hauptknotenpunkte erstellt!")
|
||||
|
||||
def initialize_database():
|
||||
"""Initialisiert die Datenbank mit Grunddaten, falls diese leer ist"""
|
||||
try:
|
||||
@@ -224,34 +198,97 @@ def initialize_database():
|
||||
# Erstelle alle Tabellen
|
||||
db.create_all()
|
||||
|
||||
# Prüfen, ob bereits Kategorien existieren
|
||||
categories_count = Category.query.count()
|
||||
users_count = User.query.count()
|
||||
|
||||
# Erstelle Standarddaten, wenn es keine Kategorien gibt
|
||||
if categories_count == 0:
|
||||
create_default_categories()
|
||||
|
||||
# Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind
|
||||
if users_count == 0:
|
||||
admin_user = User(
|
||||
# Prüfe, ob bereits Benutzer existieren
|
||||
if User.query.count() == 0:
|
||||
print("Erstelle Admin-Benutzer...")
|
||||
admin = User(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
role="admin",
|
||||
is_active=True
|
||||
is_admin=True
|
||||
)
|
||||
admin_user.set_password("admin123") # Sicheres Passwort in der Produktion verwenden!
|
||||
db.session.add(admin_user)
|
||||
admin.set_password("admin123") # In echter Umgebung ein sicheres Passwort verwenden!
|
||||
db.session.add(admin)
|
||||
|
||||
# Prüfe, ob bereits Kategorien existieren
|
||||
if Category.query.count() == 0:
|
||||
print("Erstelle Standard-Kategorien...")
|
||||
create_default_categories()
|
||||
|
||||
# Stelle sicher, dass die Standard-Knoten für die öffentliche Mindmap existieren
|
||||
if MindMapNode.query.count() == 0:
|
||||
print("Erstelle Standard-Knoten für die Mindmap...")
|
||||
|
||||
# Hauptknoten: Wissen
|
||||
root_node = MindMapNode(
|
||||
name="Wissen",
|
||||
description="Zentrale Wissensbasis",
|
||||
color_code="#4299E1",
|
||||
is_public=True
|
||||
)
|
||||
db.session.add(root_node)
|
||||
db.session.flush() # Um die ID zu generieren
|
||||
|
||||
# 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()
|
||||
print("Admin-Benutzer wurde erstellt!")
|
||||
|
||||
# Forum-Kategorien erstellen
|
||||
create_forum_categories()
|
||||
# Nachdem wir die IDs haben, füge die Verbindungen hinzu
|
||||
all_nodes = MindMapNode.query.all()
|
||||
root = MindMapNode.query.filter_by(name="Wissen").first()
|
||||
|
||||
return True
|
||||
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:
|
||||
print(f"Fehler bei Datenbank-Initialisierung: {e}")
|
||||
return False
|
||||
print(f"Fehler bei der Datenbankinitialisierung: {str(e)}")
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
||||
# 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
|
||||
@@ -278,8 +315,7 @@ def admin_required(f):
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(id):
|
||||
# Verwende session.get() anstelle von query.get() für SQLAlchemy 2.0 Kompatibilität
|
||||
return db.session.get(User, int(id))
|
||||
return User.query.get(int(id))
|
||||
|
||||
# Routes for authentication
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
@@ -289,16 +325,14 @@ def login():
|
||||
password = request.form.get('password')
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if user and user.check_password(password):
|
||||
login_user(user)
|
||||
# Aktualisiere letzten Login-Zeitpunkt
|
||||
user.last_login = datetime.now(timezone.utc)
|
||||
user.last_login = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
next_page = request.args.get('next')
|
||||
return redirect(next_page or url_for('index'))
|
||||
|
||||
flash('Ungültiger Benutzername oder Passwort')
|
||||
return render_template('login.html')
|
||||
|
||||
@@ -320,21 +354,15 @@ def register():
|
||||
user = User(username=username, email=email)
|
||||
user.set_password(password)
|
||||
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
|
||||
try:
|
||||
default_mindmap = UserMindmap(
|
||||
name='Meine Mindmap',
|
||||
description='Meine persönliche Wissenslandschaft',
|
||||
user_id=user.id
|
||||
)
|
||||
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()
|
||||
default_mindmap = UserMindmap(
|
||||
name='Meine Mindmap',
|
||||
description='Meine persönliche Wissenslandschaft',
|
||||
user=user
|
||||
)
|
||||
db.session.add(default_mindmap)
|
||||
db.session.commit()
|
||||
|
||||
login_user(user)
|
||||
flash('Dein Konto wurde erfolgreich erstellt!', 'success')
|
||||
@@ -355,130 +383,71 @@ def index():
|
||||
# Route for the mindmap page
|
||||
@app.route('/mindmap')
|
||||
def mindmap():
|
||||
"""Zeigt die Mindmap-Seite an."""
|
||||
"""Zeigt die öffentliche Mindmap an."""
|
||||
try:
|
||||
# Sicherstellen, dass wir Kategorien haben
|
||||
if Category.query.count() == 0:
|
||||
create_default_categories()
|
||||
|
||||
# Benutzer-Mindmaps, falls angemeldet
|
||||
user_mindmaps = []
|
||||
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()
|
||||
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()
|
||||
print("'Wissen'-Knoten wurde erstellt")
|
||||
# Transformiere Kategorien in ein anzeigbares Format für die Vorlage
|
||||
category_tree = [build_category_tree(cat) for cat in categories]
|
||||
|
||||
# Ü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)
|
||||
return render_template('mindmap.html', categories=category_tree)
|
||||
except Exception as e:
|
||||
# Bei Fehler leere Kategorienliste übergeben und Fehler protokollieren
|
||||
print(f"Fehler beim Laden der Mindmap-Kategorien: {str(e)}")
|
||||
return render_template('mindmap.html', categories=[], error=str(e))
|
||||
|
||||
# Route for user profile
|
||||
@app.route('/profile')
|
||||
@login_required
|
||||
def profile():
|
||||
try:
|
||||
# Versuche auf die neue Benutzermodellstruktur zuzugreifen
|
||||
_ = current_user.bio # Dies wird fehlschlagen, wenn die Spalte nicht existiert
|
||||
# Lade Benutzer-Mindmaps
|
||||
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
# Wenn keine Ausnahme, fahre mit normalem Profil fort
|
||||
# Lade Benutzer-Mindmaps
|
||||
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||
# Lade Statistiken
|
||||
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
|
||||
bookmark_count = db.session.query(user_thought_bookmark).filter(
|
||||
user_thought_bookmark.c.user_id == current_user.id).count()
|
||||
|
||||
# Prüfe, ob der Benutzer eine Standard-Mindmap hat, sonst erstelle eine
|
||||
if not user_mindmaps:
|
||||
try:
|
||||
default_mindmap = UserMindmap(
|
||||
name='Meine Mindmap',
|
||||
description='Meine persönliche Wissenslandschaft',
|
||||
user_id=current_user.id
|
||||
)
|
||||
db.session.add(default_mindmap)
|
||||
db.session.commit()
|
||||
# Berechne tatsächliche Werte für Benutzerstatistiken
|
||||
contributions_count = Comment.query.filter_by(user_id=current_user.id).count()
|
||||
|
||||
# Aktualisiere die Liste nach dem Erstellen
|
||||
user_mindmaps = [default_mindmap]
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen der Standard-Mindmap in Profil: {e}")
|
||||
# Flash-Nachricht für den Benutzer
|
||||
flash('Es gab ein Problem beim Laden deiner Mindmaps. Bitte versuche es später erneut.', 'warning')
|
||||
# Berechne Verbindungen (Anzahl der Gedankenverknüpfungen)
|
||||
connections_count = ThoughtRelation.query.filter(
|
||||
(ThoughtRelation.source_id.in_(
|
||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||
)) |
|
||||
(ThoughtRelation.target_id.in_(
|
||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||
))
|
||||
).count()
|
||||
|
||||
# Lade Statistiken
|
||||
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
|
||||
bookmark_count = db.session.query(user_thought_bookmark).filter(
|
||||
user_thought_bookmark.c.user_id == current_user.id).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
|
||||
|
||||
# Berechne tatsächliche Werte für Benutzerstatistiken
|
||||
contributions_count = Comment.query.filter_by(user_id=current_user.id).count()
|
||||
# Hole die Anzahl der Follower (falls implementiert)
|
||||
# In diesem Beispiel nehmen wir an, dass es keine Follower-Funktionalität gibt
|
||||
followers_count = 0
|
||||
|
||||
# Berechne Verbindungen (Anzahl der Gedankenverknüpfungen)
|
||||
connections_count = ThoughtRelation.query.filter(
|
||||
(ThoughtRelation.source_id.in_(
|
||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||
)) |
|
||||
(ThoughtRelation.target_id.in_(
|
||||
db.session.query(Thought.id).filter_by(user_id=current_user.id)
|
||||
))
|
||||
).count()
|
||||
# Hole den Standort des Benutzers aus der Datenbank, falls vorhanden
|
||||
location = "Deutschland" # Standardwert
|
||||
|
||||
# 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('Dein Benutzerprofil konnte nicht geladen werden. Bitte kontaktiere den Support.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
return render_template('profile.html',
|
||||
user=current_user,
|
||||
user_mindmaps=user_mindmaps,
|
||||
thought_count=thought_count,
|
||||
bookmark_count=bookmark_count,
|
||||
connections_count=connections_count,
|
||||
contributions_count=contributions_count,
|
||||
followers_count=followers_count,
|
||||
rating=round(avg_rating, 1),
|
||||
location=location)
|
||||
|
||||
# Route für Benutzereinstellungen
|
||||
@app.route('/settings', methods=['GET', 'POST'])
|
||||
@@ -629,156 +598,16 @@ def delete_mindmap(mindmap_id):
|
||||
# API-Endpunkte für Mindmap-Daten
|
||||
@app.route('/api/mindmap/public')
|
||||
def get_public_mindmap():
|
||||
"""Liefert die Standard-Mindmap-Struktur basierend auf Kategorien."""
|
||||
try:
|
||||
# Hole alle Hauptkategorien
|
||||
categories = Category.query.filter_by(parent_id=None).all()
|
||||
"""Liefert die öffentliche Mindmap-Struktur."""
|
||||
# Hole alle Kategorien der obersten Ebene
|
||||
root_categories = Category.query.filter_by(parent_id=None).all()
|
||||
|
||||
# Transformiere zu einer Baumstruktur
|
||||
category_tree = [build_category_tree(cat) for cat in categories]
|
||||
# Baue Baumstruktur auf
|
||||
result = []
|
||||
for category in root_categories:
|
||||
result.append(build_category_tree(category))
|
||||
|
||||
return jsonify(category_tree)
|
||||
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
|
||||
return jsonify(result)
|
||||
|
||||
def build_category_tree(category):
|
||||
"""
|
||||
@@ -1058,7 +887,7 @@ def update_note(note_id):
|
||||
if color_code:
|
||||
note.color_code = color_code
|
||||
|
||||
note.last_modified = datetime.now(timezone.utc)
|
||||
note.last_modified = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
@@ -1277,7 +1106,7 @@ def update_thought(thought_id):
|
||||
if 'color_code' in data:
|
||||
thought.color_code = data['color_code']
|
||||
|
||||
thought.last_modified = datetime.now(timezone.utc)
|
||||
thought.last_modified = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
@@ -1650,42 +1479,14 @@ def refresh_mindmap():
|
||||
if Category.query.count() == 0:
|
||||
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
|
||||
categories = Category.query.filter_by(parent_id=None).all()
|
||||
category_tree = [build_category_tree(cat) for cat in categories]
|
||||
|
||||
# Hole alle Mindmap-Knoten außer dem "Wissen"-Knoten
|
||||
nodes = MindMapNode.query.filter(MindMapNode.id != wissen_node.id).all()
|
||||
|
||||
# Vorbereiten der Node- und Edge-Arrays für die Antwort
|
||||
# Hole alle Mindmap-Knoten
|
||||
nodes = MindMapNode.query.all()
|
||||
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:
|
||||
node_obj = {
|
||||
'id': node.id,
|
||||
@@ -1696,28 +1497,15 @@ def refresh_mindmap():
|
||||
'category_id': node.category_id
|
||||
}
|
||||
|
||||
# Verbinde alle Top-Level-Knoten mit dem Wissen-Knoten
|
||||
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
|
||||
})
|
||||
# Verbindungen hinzufügen
|
||||
node_obj['connections'] = [{'target': child.id} for child in node.children]
|
||||
|
||||
node_data.append(node_obj)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'categories': category_tree,
|
||||
'nodes': node_data,
|
||||
'edges': edge_data
|
||||
'nodes': node_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@@ -1727,46 +1515,21 @@ def refresh_mindmap():
|
||||
'error': 'Datenbankverbindung konnte nicht hergestellt werden'
|
||||
}), 500
|
||||
|
||||
|
||||
# Route zur Mindmap HTML-Seite
|
||||
@app.route('/mindmap')
|
||||
def mindmap_page():
|
||||
return render_template('mindmap.html')
|
||||
|
||||
# Einfache Umleitungen für Community/Forum-Routen
|
||||
@app.route('/community')
|
||||
@app.route('/Community')
|
||||
@app.route('/forum')
|
||||
@app.route('/Forum')
|
||||
@app.route('/community_forum')
|
||||
def redirect_to_index():
|
||||
"""Leitet alle Community/Forum-URLs zur Startseite um"""
|
||||
return redirect(url_for('index'))
|
||||
# Fehlerbehandlung
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return jsonify({'error': 'Nicht gefunden'}), 404
|
||||
|
||||
@app.route('/static/js/mindmap-init.js')
|
||||
def serve_mindmap_init_js():
|
||||
"""Bedient die Mindmap-Initialisierungsdatei."""
|
||||
return app.send_static_file('js/mindmap-init.js'), 200, {'Content-Type': 'application/javascript'}
|
||||
@app.errorhandler(400)
|
||||
def bad_request(e):
|
||||
return jsonify({'error': 'Fehlerhafte Anfrage'}), 400
|
||||
|
||||
# Datenbank-Update-Route (admin-geschützt)
|
||||
@app.route('/admin/update-database', methods=['GET', 'POST'])
|
||||
@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)
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
return jsonify({'error': 'Serverfehler'}), 500
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,17 +0,0 @@
|
||||
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,12 +2,10 @@
|
||||
# Kopiere diese Datei zu .env und passe die Werte an
|
||||
|
||||
# Flask
|
||||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
SECRET_KEY=your-secret-key-replace-in-production
|
||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
||||
|
||||
# OpenAI API
|
||||
|
||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||
|
||||
# Datenbank
|
||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,40 +0,0 @@
|
||||
"""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 ###
|
||||
54
models.py
54
models.py
@@ -53,20 +53,11 @@ class User(db.Model, UserMixin):
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
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
|
||||
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||
messages = db.relationship('Message', backref='author', 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):
|
||||
return f'<User {self.username}>'
|
||||
@@ -77,14 +68,6 @@ class User(db.Model, UserMixin):
|
||||
def check_password(self, 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):
|
||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -323,40 +306,3 @@ class Document(db.Model):
|
||||
|
||||
def __repr__(self):
|
||||
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
53
start.sh
@@ -1,53 +0,0 @@
|
||||
#!/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 +0,0 @@
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||
#chatgpt-assistant {
|
||||
font-family: 'Inter', sans-serif;
|
||||
bottom: 4.5rem;
|
||||
}
|
||||
|
||||
#assistant-chat {
|
||||
@@ -11,7 +10,6 @@
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
#assistant-toggle {
|
||||
@@ -144,21 +142,14 @@
|
||||
.typing-indicator span {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: #888;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
opacity: 0.6;
|
||||
opacity: 0.4;
|
||||
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(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@@ -182,12 +173,11 @@ body:not(.dark) .typing-indicator span {
|
||||
@media (max-width: 640px) {
|
||||
#assistant-chat {
|
||||
width: calc(100vw - 2rem) !important;
|
||||
max-height: 70vh !important;
|
||||
}
|
||||
|
||||
#chatgpt-assistant {
|
||||
right: 1rem;
|
||||
bottom: 5rem;
|
||||
bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,26 +201,3 @@ main {
|
||||
footer {
|
||||
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;
|
||||
}
|
||||
@@ -40,8 +40,8 @@
|
||||
--light-bg: #f9fafb;
|
||||
--light-text: #1e293b;
|
||||
--light-heading: #0f172a;
|
||||
--light-primary: #7c3aed;
|
||||
--light-primary-hover: #6d28d9;
|
||||
--light-primary: #3b82f6;
|
||||
--light-primary-hover: #4f46e5;
|
||||
--light-secondary: #6b7280;
|
||||
--light-border: #e5e7eb;
|
||||
--light-card-bg: rgba(255, 255, 255, 0.92);
|
||||
@@ -68,37 +68,18 @@ body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Strikte Trennung: Dark Mode */
|
||||
html.dark body,
|
||||
body.dark {
|
||||
/* Dark Mode */
|
||||
html.dark body {
|
||||
background-color: var(--bg-primary-dark);
|
||||
color: var(--text-primary-dark);
|
||||
}
|
||||
|
||||
/* Strikte Trennung: Light Mode */
|
||||
html:not(.dark) body,
|
||||
/* Light Mode */
|
||||
body:not(.dark) {
|
||||
background-color: var(--light-bg);
|
||||
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 */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
@@ -407,7 +388,7 @@ html.dark ::-webkit-scrollbar-thumb:hover {
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.75rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,57 +457,20 @@ body:not(.dark) a:hover {
|
||||
/* Light Mode Buttons */
|
||||
body:not(.dark) .btn,
|
||||
body:not(.dark) button:not(.toggle) {
|
||||
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
||||
background-color: var(--light-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(91, 33, 182, 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);
|
||||
box-shadow: var(--light-shadow);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
body:not(.dark) .btn:hover,
|
||||
body:not(.dark) button:not(.toggle):hover {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
transform: translateY(-1px);
|
||||
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);
|
||||
background-color: var(--light-primary-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Light Mode Cards und Panels */
|
||||
@@ -580,467 +524,3 @@ body:not(.dark) .navbar {
|
||||
box-shadow: var(--light-shadow);
|
||||
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;
|
||||
}
|
||||
@@ -1,54 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate favicon.ico from SVG using cairosvg and PIL
|
||||
"""
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import io
|
||||
from cairosvg import svg2png
|
||||
from PIL import Image
|
||||
import cairosvg
|
||||
|
||||
# Verzeichnis dieses Skripts
|
||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# Pfad zum SVG-Favicon
|
||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
||||
# 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')
|
||||
|
||||
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
|
||||
"""Convert SVG to multi-size ICO file"""
|
||||
img_io = io.BytesIO()
|
||||
# SVG zu PNG konvertieren
|
||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
||||
|
||||
# Höchste Auflösung für Zwischenspeicherung
|
||||
max_size = max(sizes)
|
||||
# PNG zu ICO konvertieren
|
||||
img = Image.open(png_path)
|
||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
||||
|
||||
# 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)
|
||||
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
||||
|
||||
# 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!")
|
||||
|
||||
# Ursprüngliches Favicon konvertieren
|
||||
svg_to_ico(
|
||||
os.path.join(CURRENT_DIR, 'favicon.svg'),
|
||||
os.path.join(CURRENT_DIR, 'favicon.ico')
|
||||
)
|
||||
|
||||
# Neues Neuron-Favicon konvertieren
|
||||
svg_to_ico(
|
||||
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
|
||||
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
|
||||
)
|
||||
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
||||
# os.remove(png_path)
|
||||
@@ -1,29 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,59 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
File diff suppressed because it is too large
Load Diff
@@ -247,63 +247,130 @@ class ChatGPTAssistant {
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = sender === 'user'
|
||||
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
|
||||
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white 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%]';
|
||||
|
||||
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||
if (this.markdownParser) {
|
||||
bubble.innerHTML = this.markdownParser.parse(text);
|
||||
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
|
||||
let formattedText = '';
|
||||
|
||||
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 {
|
||||
bubble.textContent = text;
|
||||
// Für Benutzernachrichten einfache Formatierung
|
||||
formattedText = text.split('\n').map(line => {
|
||||
if (line.trim() === '') return '<br>';
|
||||
return `<p>${line}</p>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 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';
|
||||
});
|
||||
bubble.innerHTML = formattedText;
|
||||
|
||||
messageEl.appendChild(bubble);
|
||||
this.chatHistory.appendChild(messageEl);
|
||||
|
||||
// Scrolle zum Ende des Chat-Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
if (this.chatHistory) {
|
||||
this.chatHistory.appendChild(messageEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Vorschläge für mögliche Fragen an
|
||||
* @param {Array} suggestions - Array von Vorschlägen
|
||||
* Zeigt Vorschläge als klickbare Pills an
|
||||
* @param {string[]} suggestions - Liste von Vorschlägen
|
||||
*/
|
||||
showSuggestions(suggestions) {
|
||||
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
|
||||
if (!this.suggestionArea) return;
|
||||
|
||||
// Vorherige Vorschläge entfernen
|
||||
this.suggestionArea.innerHTML = '';
|
||||
|
||||
// Neue Vorschläge hinzufügen
|
||||
suggestions.forEach((text, index) => {
|
||||
const pill = document.createElement('button');
|
||||
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.style.animationDelay = `${index * 0.1}s`;
|
||||
pill.textContent = text;
|
||||
this.suggestionArea.appendChild(pill);
|
||||
});
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
suggestions.forEach(suggestion => {
|
||||
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.textContent = suggestion;
|
||||
this.suggestionArea.appendChild(pill);
|
||||
});
|
||||
|
||||
// Vorschlagsbereich anzeigen
|
||||
this.suggestionArea.classList.remove('hidden');
|
||||
this.suggestionArea.classList.remove('hidden');
|
||||
} else {
|
||||
this.suggestionArea.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -445,33 +512,26 @@ class ChatGPTAssistant {
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Ladeanimation an
|
||||
* Zeigt einen Ladeindikator im Chat an
|
||||
*/
|
||||
showLoadingIndicator() {
|
||||
if (!this.chatHistory) return;
|
||||
|
||||
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||
if (document.getElementById('assistant-loading-indicator')) return;
|
||||
// Entferne vorhandenen Ladeindikator (falls vorhanden)
|
||||
this.removeLoadingIndicator();
|
||||
|
||||
const loadingEl = document.createElement('div');
|
||||
loadingEl.id = 'assistant-loading';
|
||||
loadingEl.className = 'flex justify-start';
|
||||
loadingEl.id = 'assistant-loading-indicator';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
||||
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);
|
||||
|
||||
this.chatHistory.appendChild(loadingEl);
|
||||
|
||||
// Scroll zum Ende des Verlaufs
|
||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||
}
|
||||
|
||||
@@ -479,7 +539,7 @@ class ChatGPTAssistant {
|
||||
* Entfernt den Ladeindikator aus dem Chat
|
||||
*/
|
||||
removeLoadingIndicator() {
|
||||
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||
const loadingIndicator = document.getElementById('assistant-loading');
|
||||
if (loadingIndicator) {
|
||||
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
|
||||
this.config = {
|
||||
nodeCount: 30, // Weniger Knoten
|
||||
nodeCount: 50, // Weniger Knoten
|
||||
nodeSize: 1.2, // Kleinere Knoten
|
||||
connectionDistance: 150, // Reduzierte Verbindungsdistanz
|
||||
connectionOpacity: 0.3, // Sanftere Verbindungslinien
|
||||
clusterCount: 6, // Weniger Cluster
|
||||
clusterCount: 2, // Weniger Cluster
|
||||
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
|
||||
animationSpeed: 0.25, // Langsamere Animation
|
||||
flowDensity: 0.05, // Deutlich weniger Flussanimationen
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
{% 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,7 +6,8 @@
|
||||
<title>Systades - {% block title %}{% endblock %}</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<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.svg') }}" type="image/svg+xml">
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
||||
|
||||
<!-- Meta Tags -->
|
||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||
@@ -58,20 +59,6 @@
|
||||
800: '#0e1220',
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,8 +69,8 @@
|
||||
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome vom CDN -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||
<!-- Icons - Self-hosted Font Awesome -->
|
||||
<link href="{{ url_for('static', filename='css/all.min.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Assistent CSS -->
|
||||
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||
@@ -124,20 +111,18 @@
|
||||
<!-- Seitenspezifische Styles -->
|
||||
{% block extra_css %}{% endblock %}
|
||||
|
||||
<!-- Custom dark/light mode styles -->
|
||||
<!-- Custom dark mode styles -->
|
||||
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||
<style>
|
||||
/* Light‑Mode */
|
||||
:root {
|
||||
--bg-primary:#f8fafc;
|
||||
--bg-secondary:#f1f5f9;
|
||||
--bg-primary:#f4f6fa;
|
||||
--bg-secondary:#e9ecf3;
|
||||
--text-primary:#232837;
|
||||
--text-secondary:#475569;
|
||||
--accent-primary:#7c3aed;
|
||||
--accent-secondary:#8b5cf6;
|
||||
--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 {
|
||||
@@ -151,8 +136,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@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;
|
||||
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)] transition-colors duration-300;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
@@ -165,79 +149,6 @@
|
||||
.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); }
|
||||
.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, #6d28d9, #5b21b6);
|
||||
color: white !important;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(91, 33, 182, 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, #7c3aed, #6d28d9);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.3);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* KI-Chat Button im Light-Mode */
|
||||
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"] {
|
||||
background: linear-gradient(135deg, #7c3aed, #3b82f6);
|
||||
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, #4f46e5);
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||
@@ -247,17 +158,6 @@
|
||||
showSettingsModal: false,
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
@@ -267,7 +167,7 @@
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.darkMode = data.darkMode === 'true';
|
||||
this.applyDarkMode();
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -275,15 +175,9 @@
|
||||
});
|
||||
},
|
||||
|
||||
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() {
|
||||
this.darkMode = !this.darkMode;
|
||||
this.applyDarkMode();
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
|
||||
fetch('/api/set_dark_mode', {
|
||||
method: 'POST',
|
||||
@@ -295,6 +189,7 @@
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||
detail: { isDark: this.darkMode }
|
||||
}));
|
||||
@@ -315,7 +210,6 @@
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<!-- Logo -->
|
||||
<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>
|
||||
</a>
|
||||
|
||||
@@ -347,7 +241,7 @@
|
||||
class="nav-link flex items-center"
|
||||
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-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md 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'">
|
||||
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -363,16 +257,25 @@
|
||||
|
||||
<!-- Rechte Seite -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Dark/Light Mode Schalter -->
|
||||
<button
|
||||
@click="darkMode = !darkMode; toggleDarkMode()"
|
||||
class="theme-toggle relative w-12 h-6 rounded-full bg-gradient-to-r transition-all duration-300 flex items-center"
|
||||
:class="darkMode ? 'from-purple-700 to-indigo-800' : 'from-purple-400 to-indigo-500'"
|
||||
aria-label="Dark Mode umschalten"
|
||||
>
|
||||
<span class="absolute w-5 h-5 rounded-full bg-white shadow-md transition-transform duration-300"
|
||||
:class="darkMode ? 'translate-x-7' : 'translate-x-1'"></span>
|
||||
</button>
|
||||
<!-- Dark Mode Toggle Switch -->
|
||||
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
||||
<div class="relative w-12 h-6">
|
||||
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
||||
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
||||
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"
|
||||
x-bind:class="darkMode ? 'bg-purple-600 transform translate-x-6' : 'bg-white'"></div>
|
||||
</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 -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
@@ -509,7 +412,7 @@
|
||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
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 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
|
||||
: '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'">
|
||||
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||
</button>
|
||||
{% if current_user.is_authenticated %}
|
||||
@@ -675,72 +578,37 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||
<!-- Dark/Light-Mode persistent und robust -->
|
||||
<script>
|
||||
// Globaler Zugriff für externe Skripte
|
||||
window.MindMap = window.MindMap || {};
|
||||
|
||||
// Funktion zum Anwenden des Dark Mode, strikt getrennt
|
||||
function applyDarkModeClasses(isDarkMode) {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.body.classList.add('dark');
|
||||
localStorage.setItem('colorMode', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.body.classList.remove('dark');
|
||||
localStorage.setItem('colorMode', 'light');
|
||||
}
|
||||
|
||||
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
|
||||
const appEl = document.querySelector('body');
|
||||
if (appEl && appEl.__x) {
|
||||
appEl.__x.$data.darkMode = isDarkMode;
|
||||
}
|
||||
}
|
||||
|
||||
window.MindMap.toggleDarkMode = function() {
|
||||
const isDark = document.body.classList.contains('dark');
|
||||
applyDarkModeClasses(!isDark);
|
||||
|
||||
// Server aktualisieren
|
||||
fetch('/api/set_dark_mode', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ darkMode: !isDark })
|
||||
}).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 => {
|
||||
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);
|
||||
(function() {
|
||||
function applyMode(mode) {
|
||||
if (mode === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('colorMode', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('colorMode', 'light');
|
||||
}
|
||||
}
|
||||
// Beim Laden: Präferenz aus localStorage oder System übernehmen
|
||||
const stored = localStorage.getItem('colorMode');
|
||||
if (stored === 'dark' || stored === 'light') {
|
||||
applyMode(stored);
|
||||
} else {
|
||||
// Systempräferenz als Fallback
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
applyMode(prefersDark ? 'dark' : 'light');
|
||||
}
|
||||
// Umschalter für alle Mode-Toggles
|
||||
window.toggleColorMode = function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
applyMode(isDark ? 'light' : 'dark');
|
||||
};
|
||||
// Optional: globales Event für andere Skripte
|
||||
window.addEventListener('storage', function(e) {
|
||||
if (e.key === 'colorMode') applyMode(e.newValue);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,192 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,344 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,125 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,355 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,491 +0,0 @@
|
||||
{% 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 %}
|
||||
{{ post.author.username[0].upper() }}
|
||||
{% 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 %}
|
||||
{{ reply.author.username[0].upper() }}
|
||||
{% 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 %}
|
||||
@@ -1,137 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,316 +0,0 @@
|
||||
{% 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>
|
||||
</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 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;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -39,35 +39,6 @@
|
||||
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-2 { animation-delay: 0.4s; }
|
||||
.delay-3 { animation-delay: 0.6s; }
|
||||
@@ -100,20 +71,16 @@
|
||||
|
||||
/* Chat section styles */
|
||||
.embedded-chat {
|
||||
height: 500px;
|
||||
height: 350px;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
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 {
|
||||
background-color: rgba(17, 24, 39, 0.7);
|
||||
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 {
|
||||
@@ -122,118 +89,9 @@
|
||||
}
|
||||
|
||||
#embedded-chat-messages {
|
||||
flex: 1;
|
||||
height: 250px;
|
||||
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;
|
||||
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 */
|
||||
@@ -273,9 +131,16 @@
|
||||
<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">
|
||||
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
||||
<div class="overflow-hidden flex justify-center gap-6">
|
||||
<span class="relative inline-block text-reveal marker-animation">Wissen.</span>
|
||||
<span class="relative inline-block text-reveal delay-1 marker-animation marker-animation-delay">Vernetzen.</span>
|
||||
<div class="overflow-hidden">
|
||||
<span class="gradient-text inline-block text-reveal">Wissen</span>
|
||||
</div>
|
||||
<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>
|
||||
</h1>
|
||||
<div class="overflow-hidden">
|
||||
@@ -314,12 +179,6 @@
|
||||
<div class="text-center">
|
||||
<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>
|
||||
<!-- 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>
|
||||
@@ -412,14 +271,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<div id="embedded-chat-messages" class="border-b-0">
|
||||
<div id="embedded-chat-messages" class="border-b border-gray-200 dark:border-gray-700">
|
||||
<!-- Assistant Message -->
|
||||
<div class="chat-message flex">
|
||||
<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">
|
||||
<div class="mb-4 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">
|
||||
<i class="fa-solid fa-robot text-sm"></i>
|
||||
</div>
|
||||
<div class="chat-bubble assistant-bubble">
|
||||
<div class="markdown-content">
|
||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
||||
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
||||
<p>Hallo! Ich bin dein Systades-Assistent. Wie kann ich dir heute helfen?</p>
|
||||
<p>Du kannst mir Fragen zu:</p>
|
||||
<ul>
|
||||
@@ -433,24 +292,24 @@
|
||||
</div>
|
||||
|
||||
<!-- User Message -->
|
||||
<div class="chat-message flex justify-end">
|
||||
<div class="chat-bubble user-bubble">
|
||||
<p>
|
||||
<div class="mb-4 flex justify-end">
|
||||
<div class="bg-purple-100 dark:bg-purple-900/30 rounded-lg p-3 max-w-[80%]">
|
||||
<p class="text-gray-800 dark:text-gray-200">
|
||||
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
|
||||
</p>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<i class="fa-solid fa-user text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assistant Response -->
|
||||
<div class="chat-message flex" id="demo-ai-response">
|
||||
<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">
|
||||
<div class="mb-4 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">
|
||||
<i class="fa-solid fa-robot text-sm"></i>
|
||||
</div>
|
||||
<div class="chat-bubble assistant-bubble">
|
||||
<div class="markdown-content">
|
||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
|
||||
<div class="text-gray-700 dark:text-gray-300 markdown-content">
|
||||
<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>
|
||||
<ol>
|
||||
@@ -466,19 +325,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Chat Input -->
|
||||
<div class="chat-input-container">
|
||||
<div class="p-4">
|
||||
<div class="flex">
|
||||
<input type="text" placeholder="Stelle eine Frage..." class="mystical-input flex-grow" 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>
|
||||
<button class="ml-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white p-2 rounded-lg disabled:opacity-50" disabled>
|
||||
<i class="fa-solid fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Quick Queries -->
|
||||
<div class="quick-query-container">
|
||||
<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 hover:shadow-sm">KI-Grundlagen</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 hover:shadow-sm">Datenbank durchsuchen</button>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<span class="text-xs 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="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="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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,357 +1,234 @@
|
||||
{% extends "base.html" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interaktive Mindmap</title>
|
||||
|
||||
{% block title %}Mindmap{% endblock %}
|
||||
<!-- Cytoscape.js -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||
|
||||
{% block extra_css %}
|
||||
<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;
|
||||
}
|
||||
<!-- Socket.IO -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
||||
|
||||
.mindmap-container {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
<!-- Feather Icons (optional) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
||||
|
||||
.dark .mindmap-container {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.mindmap-toolbar {
|
||||
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;
|
||||
}
|
||||
|
||||
body:not(.dark) .mindmap-toolbar {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.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;
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex flex-col lg:flex-row gap-8">
|
||||
<!-- Hauptinhalt -->
|
||||
<div class="w-full lg:w-3/4">
|
||||
<!-- Mindmap-Titelbereich -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold mb-2 mystical-glow"
|
||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||
Wissenslandkarte
|
||||
</h1>
|
||||
<p class="opacity-80 text-lg"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
Visualisiere die Verbindungen zwischen Gedanken und Konzepten
|
||||
</p>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f9fafb;
|
||||
color: #111827;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #1f2937;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.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>
|
||||
</header>
|
||||
|
||||
<!-- Mindmap-Container -->
|
||||
<div class="mindmap-container">
|
||||
<!-- Toolbar -->
|
||||
<div class="mindmap-toolbar">
|
||||
<button id="fit-btn" class="mindmap-action-btn">
|
||||
<i class="fa-solid fa-expand"></i>
|
||||
<span>Ansicht anpassen</span>
|
||||
</button>
|
||||
<button id="reset-btn" class="mindmap-action-btn">
|
||||
<i class="fa-solid fa-undo"></i>
|
||||
<span>Zurücksetzen</span>
|
||||
</button>
|
||||
<button id="toggle-labels-btn" class="mindmap-action-btn">
|
||||
<i class="fa-solid fa-tags"></i>
|
||||
<span>Labels ein/aus</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hauptvisualisierung -->
|
||||
<div id="cy"></div>
|
||||
|
||||
<!-- Info-Panel -->
|
||||
<div id="node-info-panel" class="mindmap-info-panel">
|
||||
<h4 class="info-panel-title">Knoteninfo</h4>
|
||||
<p id="node-description" class="info-panel-description">Wählen Sie einen Knoten aus...</p>
|
||||
|
||||
<div class="node-navigation">
|
||||
<h5 class="node-navigation-title">Verknüpfte Knoten</h5>
|
||||
<div id="connected-nodes" class="node-links">
|
||||
<!-- Wird dynamisch befüllt -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Seitenleiste -->
|
||||
<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 id="category-filters" class="category-filters">
|
||||
<!-- Wird dynamisch befüllt -->
|
||||
</div>
|
||||
|
||||
<div id="cy"></div>
|
||||
|
||||
<footer class="footer">
|
||||
Mindmap-Anwendung © 2023
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- Cytoscape.js laden -->
|
||||
<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>
|
||||
<!-- Unsere Mindmap JS -->
|
||||
<script src="{{ url_for('static', filename='js/mindmap.js') }}"></script>
|
||||
|
||||
<!-- Mindmap-Initialisierer laden -->
|
||||
<script src="/static/js/mindmap-init.js"></script>
|
||||
|
||||
<script>
|
||||
// Sobald die Seite und die Scripte geladen sind, initialisiere die Mindmap
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Die Initialisierung wird jetzt direkt in mindmap-init.js ausgeführt
|
||||
console.log('Mindmap-Seite geladen');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
<!-- Icons initialisieren -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof feather !== 'undefined') {
|
||||
feather.replace();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -381,143 +381,103 @@
|
||||
}
|
||||
|
||||
/* Light Mode Anpassungen */
|
||||
body:not(.dark) .profile-container,
|
||||
body:not(.dark) .profile-tabs,
|
||||
body:not(.dark) .activity-card,
|
||||
body:not(.dark) .settings-card {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
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);
|
||||
}
|
||||
|
||||
body:not(.dark) .glass-card {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
body:not(.dark) .avatar-container {
|
||||
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);
|
||||
}
|
||||
|
||||
body:not(.dark) .user-info h1 {
|
||||
html.light .user-info h1 {
|
||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
body:not(.dark) .user-bio,
|
||||
body:not(.dark) .activity-content {
|
||||
html.light .user-bio,
|
||||
html.light .activity-content {
|
||||
color: #1a202c;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
body:not(.dark) .user-meta span {
|
||||
html.light .user-meta span {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
body:not(.dark) .stat-item,
|
||||
body:not(.dark) .settings-input {
|
||||
html.light .stat-item,
|
||||
html.light .settings-input {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .stat-value {
|
||||
html.light .stat-value {
|
||||
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||
}
|
||||
|
||||
body:not(.dark) .stat-label {
|
||||
html.light .stat-label {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
body:not(.dark) .profile-tab {
|
||||
html.light .profile-tab {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
body:not(.dark) .profile-tab:hover {
|
||||
html.light .profile-tab:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
body:not(.dark) .profile-tab.active {
|
||||
html.light .profile-tab.active {
|
||||
color: #7e3ff2;
|
||||
border-bottom: 3px solid #7e3ff2;
|
||||
background: rgba(126, 63, 242, 0.08);
|
||||
}
|
||||
|
||||
body:not(.dark) .activity-title,
|
||||
body:not(.dark) .settings-card-header,
|
||||
body:not(.dark) .settings-label {
|
||||
html.light .activity-title,
|
||||
html.light .settings-card-header,
|
||||
html.light .settings-label {
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
body:not(.dark) .activity-date {
|
||||
html.light .activity-date {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
body:not(.dark) .activity-footer {
|
||||
html.light .activity-footer {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .reaction-button {
|
||||
html.light .reaction-button {
|
||||
color: #4a5568;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
body:not(.dark) .reaction-button:hover {
|
||||
html.light .reaction-button:hover {
|
||||
background: rgba(126, 63, 242, 0.1);
|
||||
color: #7e3ff2;
|
||||
}
|
||||
|
||||
body:not(.dark) .reaction-button.active {
|
||||
html.light .reaction-button.active {
|
||||
background: rgba(126, 63, 242, 0.15);
|
||||
color: #7e3ff2;
|
||||
}
|
||||
|
||||
body:not(.dark) .action-button {
|
||||
html.light .action-button {
|
||||
background: rgba(126, 63, 242, 0.1);
|
||||
color: #7e3ff2;
|
||||
border: 1px solid rgba(126, 63, 242, 0.2);
|
||||
}
|
||||
|
||||
body:not(.dark) .action-button:hover {
|
||||
html.light .action-button:hover {
|
||||
background: rgba(126, 63, 242, 0.2);
|
||||
}
|
||||
|
||||
/* Verbesserte Styles für Card-Items im Light Mode */
|
||||
body:not(.dark) .thought-item,
|
||||
body:not(.dark) .mindmap-item,
|
||||
body:not(.dark) .collection-item {
|
||||
background: white !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .thought-item:hover,
|
||||
body:not(.dark) .mindmap-item:hover,
|
||||
body:not(.dark) .collection-item:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-3px);
|
||||
border: 1px solid rgba(126, 63, 242, 0.2) !important;
|
||||
}
|
||||
|
||||
body:not(.dark) .edit-profile-btn {
|
||||
background: #7e3ff2;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
body:not(.dark) .edit-profile-btn:hover {
|
||||
background: #6d28d9;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.25);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -599,7 +559,6 @@
|
||||
<div class="profile-tabs">
|
||||
<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="mindmaps">Mindmaps</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="settings">Einstellungen</div>
|
||||
@@ -653,87 +612,21 @@
|
||||
<div class="tab-content hidden" id="thoughts-tab">
|
||||
<div id="thoughts-container">
|
||||
{% if thoughts %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for thought in thoughts %}
|
||||
<div class="thought-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" style="border-left: 4px solid {{ thought.color_code|default('#B39DDB') }}">
|
||||
<h3 class="text-xl font-bold mb-2"
|
||||
x-bind:class="darkMode ? 'text-purple-300' : 'text-purple-700'">{{ thought.title }}</h3>
|
||||
<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 class="thought-item">
|
||||
<h3>{{ thought.title }}</h3>
|
||||
<p>{{ thought.content }}</p>
|
||||
<div class="thought-meta">
|
||||
<span>{{ thought.date }}</span>
|
||||
<span>{{ thought.category }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Gedanken erstellt</p>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -756,7 +649,7 @@
|
||||
<div class="text-center py-12">
|
||||
<i class="fas fa-folder-open text-5xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">Noch keine Sammlungen erstellt</p>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -790,7 +683,7 @@
|
||||
<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 Verbindungen erstellt</p>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,391 +0,0 @@
|
||||
{% 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
65
update_db.py
@@ -1,65 +0,0 @@
|
||||
#!/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(
|
||||
username=username,
|
||||
email=email,
|
||||
role='admin' if is_admin else 'user',
|
||||
is_admin=is_admin,
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
user.set_password(password)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
home = C:\Program Files\Python313
|
||||
include-system-site-packages = false
|
||||
version = 3.13.3
|
||||
executable = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe
|
||||
command = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe -m venv C:\Users\firem\Desktop\111\Systades\website\venv
|
||||
executable = C:\Program Files\Python313\python.exe
|
||||
command = C:\Program Files\Python313\python.exe -m venv C:\Users\TTOMCZA.EMEA\Dev\website\venv
|
||||
|
||||
58
windows_setup.bat
Normal file
58
windows_setup.bat
Normal file
@@ -0,0 +1,58 @@
|
||||
@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