Compare commits

..

50 Commits

Author SHA1 Message Date
693e542d5f UTF8 in .env, FLASK DEBUG 2025-05-02 08:06:38 +01:00
4c3e476338 feat: update environment config and add community preview template 2025-05-02 08:01:23 +01:00
613c38ccb2 🔧 chore: update environment variables in .env file 2025-05-02 07:30:58 +01:00
91fdd43fe0 🔒 chore: entferne den OpenAI API-Schlüssel aus der Beispielumgebung für Sicherheitsgründe 2025-05-01 21:15:04 +01:00
f36dd5ffaa OpenAI Api Key 2025-05-01 21:10:19 +01:00
2e1c3ce8b0 Community erstellt 2025-05-01 21:04:37 +01:00
d80c4c9aec feat(models): update model structure for improved data handling 2025-05-01 20:56:05 +01:00
3b0bea959c 🔧 fix: update venv configuration for improved compatibility 2025-05-01 20:25:01 +01:00
cb3bfe0e6a LOL 2025-05-01 19:57:26 +01:00
fd63810845 feat: update environment and scripts for improved functionality 2025-05-01 16:29:57 +02:00
883973fe7b feat: update environment variables and enhance mindmap functionality 2025-05-01 16:27:40 +02:00
027e632856 feat: update example.env with new configuration settings 2025-05-01 16:24:29 +02:00
406289e54f 🎨 feat(mindmap): improve rendering performance and optimize code structure 2025-05-01 16:16:26 +02:00
71b33e6cec feat(mindmap): enhance mindmap rendering performance and responsiveness 2025-05-01 16:14:14 +02:00
c74d3164bb 🎨 feat: update mindmap templates and JS module for improved UI design 2025-05-01 16:11:42 +02:00
4982cddeef 🎉 feat: update Dockerfile and scripts for improved functionality 2025-05-01 16:05:52 +02:00
631619ccb4 feat: update docker-compose.yml for improved service configuration 2025-05-01 16:01:00 +02:00
f9881b678d 🔄 chore: aktualisiere die .env-Datei für verbesserte Konfiguration 2025-05-01 11:27:11 +01:00
259ce3cf69 feat: update Dockerfile and docker-compose for improved build process 2025-05-01 11:24:27 +01:00
9f4743eaea feat: update cached Python files and add new static image asset 2025-05-01 10:55:30 +01:00
de0f837cfd Optimierung der Projektstruktur: Entferne nicht mehr benötigte Skripte und Dateien, um die Wartbarkeit zu erhöhen und veraltete Komponenten zu beseitigen. 2025-04-30 15:51:07 +02:00
1c49ddfb19 chore: automatic commit 2025-04-30 15:49 2025-04-30 15:49:18 +02:00
46c16e5f01 chore: automatic commit 2025-04-30 15:44 2025-04-30 15:44:02 +02:00
84667bca00 chore: automatic commit 2025-04-30 15:41 2025-04-30 15:41:00 +02:00
779449559d chore: automatic commit 2025-04-30 15:38 2025-04-30 15:38:56 +02:00
721a10e861 Entferne nicht mehr benötigte Skripte: Lösche die Dateien check_schema.py, create_default_users.py, fix_user_table.py, test_app.py und windows_setup.bat, um die Projektstruktur zu optimieren und veraltete Komponenten zu entfernen. 2025-04-30 15:33:39 +02:00
a431873ca2 chore: automatic commit 2025-04-30 15:29 2025-04-30 15:29:23 +02:00
e4ab1e1bb5 chore: automatic commit 2025-04-30 12:48 2025-04-30 12:48:06 +02:00
f69356473b Entferne nicht mehr benötigte Dateien: Lösche docker-compose.yml, Dockerfile, README.md, requirements.txt, start_server.bat, start-flask-server.py, start.sh, test_server.py, sowie alle zugehörigen Datenbank- und Website-Dateien. Diese Bereinigung optimiert die Projektstruktur und entfernt veraltete Komponenten. 2025-04-30 12:34:06 +02:00
38ac13e87c chore: automatic commit 2025-04-30 12:32 2025-04-30 12:32:36 +02:00
0afb8cb6e2 Update neural network background configuration: reduce node count and connection distance, adjust glow and node colors, and modify shadow blur for improved visual clarity and performance. 2025-04-29 20:58:27 +01:00
5d282d2108 Refactor neural network background animation: streamline the code by consolidating node and connection logic, enhancing visual effects with improved glow and animation dynamics. Introduce responsive canvas resizing and optimize particle behavior for a smoother experience. 2025-04-29 20:54:24 +01:00
4aba72efa2 Merge branch 'main' of https://git.clickcandit.com/marwinm/website 2025-04-29 20:52:11 +01:00
89476d5353 w 2025-04-29 20:51:49 +01:00
0f7a33340a Update mindmap database: replace binary file with a new version to reflect recent changes in structure and data. 2025-04-27 08:56:56 +01:00
73501e7cda Add Flask server startup scripts: introduce start_server.bat for Windows and start-flask-server.py for enhanced server management. Update run.py to include logging and threaded request handling. Add test_server.py for server accessibility testing. 2025-04-25 17:09:09 +01:00
9f8eba6736 Refactor database initialization: streamline the process by removing the old init_database function, implementing a new structure for database setup, and ensuring the creation of a comprehensive mindmap hierarchy with an admin user. Update app.py to run on port 5000 instead of 6000. 2025-04-21 18:43:58 +01:00
b6bf9f387d Update mindmap database: replace binary file with a new version to incorporate recent structural and data changes. 2025-04-21 18:26:41 +01:00
d9fe1f8efc Update mindmap database: replace existing binary file with a new version, reflecting recent changes in mindmap structure and data. 2025-04-20 20:28:51 +01:00
fd7bc59851 Add user authentication routes: implement login, registration, and logout functionality, along with user profile and admin routes. Enhance mindmap API with error handling and default node creation. 2025-04-20 19:58:27 +01:00
55f1f87509 Refactor app initialization: encapsulate Flask app setup and database initialization within a create_app function, improving modularity and error handling during startup. 2025-04-20 19:54:07 +01:00
03f8761312 Update Docker configuration to change exposed port from 5000 to 6000 in Dockerfile and docker-compose.yml, ensuring consistency across the application. 2025-04-20 19:48:49 +01:00
506748fda7 Implement error handling and default node creation for mindmap routes; initialize database on first request to ensure root node exists. 2025-04-20 19:43:21 +01:00
6d069f68cd Update requirements.txt to include new dependencies for enhanced functionality and remove outdated packages for better compatibility. 2025-04-20 19:32:32 +01:00
4310239a7a Enhance Dockerfile: add system dependencies for building Python packages, update requirements.txt to remove specific version constraints, and verify installations with pip list. 2025-04-20 19:31:13 +01:00
e9fe907af0 Update requirements.txt to include email_validator==2.1.1 for improved email validation functionality. 2025-04-20 19:29:19 +01:00
0c69d9aba3 Remove unnecessary 'force' option from docker-compose.yml for cleaner configuration. 2025-04-20 19:26:44 +01:00
6da85cdece Refactor Docker setup: update docker-compose.yml to use a specific website directory for volumes, enable automatic restarts, and modify Dockerfile to clean up and copy application files more efficiently. 2025-04-20 19:25:08 +01:00
a073b09115 Update Docker configuration: change Docker Compose version to 3.8, enhance web service setup with context and dockerfile specifications, add volume and environment variables for Flask development, and modify Dockerfile to use Python 3.11 and improve file copying and command execution. 2025-04-20 19:22:08 +01:00
f1f4870989 Update dependencies in requirements.txt to specific versions for Flask, Flask-Login, Flask-SQLAlchemy, Werkzeug, and SQLAlchemy 2025-04-20 19:16:34 +01:00
37 changed files with 3118 additions and 1147 deletions

17
.env
View File

@@ -1,2 +1,15 @@
SECRET_KEY=eed9298856dc9363cd32778265780d6904ba24e6a6b815a2cc382bcdd767ea7b # MindMap Umgebungsvariablen
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier # Kopiere diese Datei zu .env und passe die Werte an
# Flask
FLASK_APP=app.py
FLASK_DEBUG=1
SECRET_KEY=your-secret-key-replace-in-production
# OpenAI API
OPENAI_API_KEY=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
# Datenbank
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
# Der Pfad wird relativ zum Projektverzeichnis angegeben
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
# Dockerfile
FROM python:3.11-slim
# Arbeitsverzeichnis in Container
WORKDIR /app
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /app/database
# pip auf den neuesten Stand bringen und Requirements installieren
COPY requirements.txt ./
RUN pip install --upgrade pip && \
pip install --no-cache-dir -U -r requirements.txt
# Anwendungscode kopieren
COPY . .
# Berechtigungen für database-Ordner
RUN chmod -R 777 /app/database
# Exponiere Port 5000 für Flask
EXPOSE 5000
# Setze Umgebungsvariablen
ENV FLASK_APP=app.py
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Startkommando mit spezifischen Flags für Produktion
CMD ["python", "app.py"]

Binary file not shown.

Binary file not shown.

410
app.py
View File

@@ -24,7 +24,7 @@ from flask_migrate import Migrate
from models import ( from models import (
db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating,
RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote, RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote,
node_thought_association, user_thought_bookmark, node_relationship node_thought_association, user_thought_bookmark, node_relationship, ForumCategory, ForumPost
) )
# Lade .env-Datei # Lade .env-Datei
@@ -190,6 +190,31 @@ def create_default_categories():
db.session.commit() db.session.commit()
print("Standard-Kategorien wurden erstellt!") 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(): def initialize_database():
"""Initialisiert die Datenbank mit Grunddaten, falls diese leer ist""" """Initialisiert die Datenbank mit Grunddaten, falls diese leer ist"""
try: try:
@@ -198,97 +223,34 @@ def initialize_database():
# Erstelle alle Tabellen # Erstelle alle Tabellen
db.create_all() db.create_all()
# Prüfe, ob bereits Benutzer existieren # Prüfen, ob bereits Kategorien existieren
if User.query.count() == 0: categories_count = Category.query.count()
print("Erstelle Admin-Benutzer...") users_count = User.query.count()
admin = User(
username="admin",
email="admin@example.com",
is_admin=True
)
admin.set_password("admin123") # In echter Umgebung ein sicheres Passwort verwenden!
db.session.add(admin)
# Prüfe, ob bereits Kategorien existieren # Erstelle Standarddaten, wenn es keine Kategorien gibt
if Category.query.count() == 0: if categories_count == 0:
print("Erstelle Standard-Kategorien...")
create_default_categories() create_default_categories()
# Stelle sicher, dass die Standard-Knoten für die öffentliche Mindmap existieren # Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind
if MindMapNode.query.count() == 0: if users_count == 0:
print("Erstelle Standard-Knoten für die Mindmap...") admin_user = User(
username="admin",
# Hauptknoten: Wissen email="admin@example.com",
root_node = MindMapNode( role="admin",
name="Wissen", is_active=True
description="Zentrale Wissensbasis",
color_code="#4299E1",
is_public=True
) )
db.session.add(root_node) admin_user.set_password("admin123") # Sicheres Passwort in der Produktion verwenden!
db.session.flush() # Um die ID zu generieren db.session.add(admin_user)
# Verwandte Kategorien finden
philosophy = Category.query.filter_by(name="Philosophie").first()
science = Category.query.filter_by(name="Wissenschaft").first()
technology = Category.query.filter_by(name="Technologie").first()
arts = Category.query.filter_by(name="Künste").first()
# Erstelle Hauptthemenknoten
nodes = [
MindMapNode(
name="Philosophie",
description="Philosophisches Denken",
color_code="#9F7AEA",
category=philosophy,
is_public=True
),
MindMapNode(
name="Wissenschaft",
description="Wissenschaftliche Erkenntnisse",
color_code="#48BB78",
category=science,
is_public=True
),
MindMapNode(
name="Technologie",
description="Technologische Entwicklungen",
color_code="#ED8936",
category=technology,
is_public=True
),
MindMapNode(
name="Künste",
description="Künstlerische Ausdrucksformen",
color_code="#ED64A6",
category=arts,
is_public=True
)
]
# Füge Knoten zur Datenbank hinzu
for node in nodes:
db.session.add(node)
db.session.commit() db.session.commit()
print("Admin-Benutzer wurde erstellt!")
# Nachdem wir die IDs haben, füge die Verbindungen hinzu
all_nodes = MindMapNode.query.all() # Forum-Kategorien erstellen
root = MindMapNode.query.filter_by(name="Wissen").first() create_forum_categories()
if root: return True
for node in all_nodes:
if node.id != root.id:
root.children.append(node)
# Speichere die Änderungen
db.session.commit()
print("Datenbankinitialisierung abgeschlossen.")
except Exception as e: except Exception as e:
print(f"Fehler bei der Datenbankinitialisierung: {str(e)}") print(f"Fehler bei Datenbank-Initialisierung: {e}")
db.session.rollback() return False
raise
# Instead of before_first_request, which is deprecated in newer Flask versions # Instead of before_first_request, which is deprecated in newer Flask versions
# Use a function to initialize the database that will be called during app creation # Use a function to initialize the database that will be called during app creation
@@ -1515,12 +1477,286 @@ def refresh_mindmap():
'error': 'Datenbankverbindung konnte nicht hergestellt werden' 'error': 'Datenbankverbindung konnte nicht hergestellt werden'
}), 500 }), 500
# Route zur Mindmap HTML-Seite # Route zur Mindmap HTML-Seite
@app.route('/mindmap') @app.route('/mindmap')
def mindmap_page(): def mindmap_page():
return render_template('mindmap.html') return render_template('mindmap.html')
# Weiterleitung für Groß-/Kleinschreibung
@app.route('/Community')
def Community():
# Direkte Umleitung ohne url_for zu verwenden, um Kreisverweise zu vermeiden
return redirect('/')
# Vermeidung der direkten Community-Route, die Probleme verursacht
@app.route('/community')
def community():
"""Vermeidet direkten Zugriff auf Community-Forum"""
return redirect('/')
# Alternative Route für Community-Forum, die über das Menü erreichbar ist
@app.route('/forum')
def forum():
"""Alternative Route zum Community-Forum"""
if not current_user.is_authenticated:
# Wenn Benutzer nicht angemeldet ist, zeige eine Vorschauversion
forum_categories = ForumCategory.query.filter_by(is_active=True).limit(5).all()
categories_data = []
for category in forum_categories:
total_posts = ForumPost.query.filter_by(category_id=category.id, parent_id=None).count()
total_replies = ForumPost.query.filter(
ForumPost.category_id == category.id,
ForumPost.parent_id != None
).count()
categories_data.append({
'category': category,
'total_posts': total_posts,
'total_replies': total_replies,
'latest_post': None
})
return render_template('community/preview.html', categories_data=categories_data)
# Angemeldete Benutzer bekommen die vollständige Version
forum_categories = ForumCategory.query.filter_by(is_active=True).all()
categories_data = []
for category in forum_categories:
total_posts = ForumPost.query.filter_by(category_id=category.id, parent_id=None).count()
total_replies = ForumPost.query.filter(
ForumPost.category_id == category.id,
ForumPost.parent_id != None
).count()
latest_post = ForumPost.query.filter_by(category_id=category.id)\
.order_by(ForumPost.created_at.desc()).first()
categories_data.append({
'category': category,
'total_posts': total_posts,
'total_replies': total_replies,
'latest_post': latest_post
})
return render_template('community/index.html', categories_data=categories_data)
@app.route('/forum/category/<int:category_id>')
@login_required
def forum_category(category_id):
"""Zeigt alle Themen in einer bestimmten Kategorie an"""
category = ForumCategory.query.get_or_404(category_id)
# Haupt-Beiträge (Threads) in dieser Kategorie
threads = ForumPost.query.filter_by(
category_id=category_id,
parent_id=None
).order_by(ForumPost.is_pinned.desc(), ForumPost.created_at.desc()).all()
# Zähle Antworten und hole den neuesten Beitrag für jeden Thread
threads_data = []
for thread in threads:
reply_count = ForumPost.query.filter_by(parent_id=thread.id).count()
latest_reply = ForumPost.query.filter_by(parent_id=thread.id)\
.order_by(ForumPost.created_at.desc()).first()
threads_data.append({
'thread': thread,
'reply_count': reply_count,
'latest_reply': latest_reply
})
return render_template('community/category.html',
category=category,
threads_data=threads_data,
node=category.node)
@app.route('/forum/post/<int:post_id>')
@login_required
def forum_post(post_id):
"""Zeigt einen Beitrag und alle seine Antworten an"""
post = ForumPost.query.get_or_404(post_id)
# Wenn es sich um eine Antwort handelt, leite zur übergeordneten Beitragsseite weiter
if post.parent_id:
return redirect(url_for('forum_post', post_id=post.parent_id))
# Erhöhe die Ansichtszahl
post.view_count += 1
db.session.commit()
# Hole alle Antworten zu diesem Beitrag
replies = ForumPost.query.filter_by(parent_id=post_id)\
.order_by(ForumPost.created_at).all()
return render_template('community/post.html',
post=post,
replies=replies,
category=post.category)
@app.route('/forum/new-post/<int:category_id>', methods=['GET', 'POST'])
@login_required
def new_post(category_id):
"""Erstellt einen neuen Beitrag in einer Kategorie"""
category = ForumCategory.query.get_or_404(category_id)
if request.method == 'POST':
title = request.form.get('title')
content = request.form.get('content')
if not title or not content:
flash('Bitte fülle alle Pflichtfelder aus.', 'error')
return redirect(url_for('new_post', category_id=category_id))
# Neuen Beitrag erstellen
post = ForumPost(
title=title,
content=content,
user_id=current_user.id,
category_id=category_id
)
db.session.add(post)
db.session.commit()
flash('Dein Beitrag wurde erfolgreich erstellt.', 'success')
return redirect(url_for('forum_post', post_id=post.id))
return render_template('community/new_post.html', category=category)
@app.route('/forum/reply/<int:post_id>', methods=['POST'])
@login_required
def reply_to_post(post_id):
"""Antwortet auf einen bestehenden Beitrag"""
parent_post = ForumPost.query.get_or_404(post_id)
# Stelle sicher, dass der Beitrag nicht gesperrt ist
if parent_post.is_locked:
flash('Dieser Beitrag ist gesperrt und kann keine Antworten mehr erhalten.', 'error')
return redirect(url_for('forum_post', post_id=post_id))
content = request.form.get('content')
if not content:
flash('Die Antwort darf nicht leer sein.', 'error')
return redirect(url_for('forum_post', post_id=post_id))
# Erstelle eine Antwort
reply = ForumPost(
title=f"Re: {parent_post.title}",
content=content,
user_id=current_user.id,
category_id=parent_post.category_id,
parent_id=post_id
)
db.session.add(reply)
db.session.commit()
flash('Deine Antwort wurde erfolgreich hinzugefügt.', 'success')
return redirect(url_for('forum_post', post_id=post_id))
@app.route('/forum/edit-post/<int:post_id>', methods=['GET', 'POST'])
@login_required
def edit_post(post_id):
"""Bearbeitet einen bestehenden Beitrag"""
post = ForumPost.query.get_or_404(post_id)
# Überprüfe, ob der Benutzer der Autor ist oder Admin-Rechte hat
if post.user_id != current_user.id and current_user.role != 'admin':
flash('Du hast keine Berechtigung, diesen Beitrag zu bearbeiten.', 'error')
return redirect(url_for('forum_post', post_id=post.parent_id or post.id))
if request.method == 'POST':
title = request.form.get('title')
content = request.form.get('content')
if not title or not content:
flash('Bitte fülle alle Pflichtfelder aus.', 'error')
return redirect(url_for('edit_post', post_id=post_id))
# Aktualisiere den Beitrag
post.title = title
post.content = content
post.updated_at = datetime.utcnow()
db.session.commit()
flash('Dein Beitrag wurde erfolgreich aktualisiert.', 'success')
return redirect(url_for('forum_post', post_id=post.parent_id or post.id))
return render_template('community/edit_post.html', post=post)
@app.route('/forum/delete-post/<int:post_id>', methods=['POST'])
@login_required
def delete_post(post_id):
"""Löscht einen Beitrag"""
post = ForumPost.query.get_or_404(post_id)
# Überprüfe, ob der Benutzer der Autor ist oder Admin-Rechte hat
if post.user_id != current_user.id and current_user.role != 'admin':
flash('Du hast keine Berechtigung, diesen Beitrag zu löschen.', 'error')
return redirect(url_for('forum_post', post_id=post.parent_id or post.id))
# Bestimme, wohin nach dem Löschen weitergeleitet wird
if post.parent_id:
redirect_url = url_for('forum_post', post_id=post.parent_id)
else:
redirect_url = url_for('forum_category', category_id=post.category_id)
# Lösche den Beitrag und seine Antworten
if not post.parent_id: # Wenn es ein Hauptbeitrag ist, lösche auch alle Antworten
ForumPost.query.filter_by(parent_id=post_id).delete()
db.session.delete(post)
db.session.commit()
flash('Der Beitrag wurde erfolgreich gelöscht.', 'success')
return redirect(redirect_url)
@app.route('/forum/toggle-pin/<int:post_id>', methods=['POST'])
@login_required
def toggle_pin_post(post_id):
"""Fixiert oder löst einen Beitrag von der Fixierung"""
# Nur Admins und Moderatoren können Beiträge fixieren
if current_user.role not in ['admin', 'moderator']:
flash('Du hast keine Berechtigung, Beiträge zu fixieren.', 'error')
return redirect(url_for('forum_post', post_id=post_id))
post = ForumPost.query.get_or_404(post_id)
# Nur Hauptbeiträge können fixiert werden
if post.parent_id:
flash('Nur Hauptbeiträge können fixiert werden.', 'error')
return redirect(url_for('forum_post', post_id=post.parent_id))
# Ändere den Status
post.is_pinned = not post.is_pinned
db.session.commit()
status = 'fixiert' if post.is_pinned else 'nicht mehr fixiert'
flash(f'Der Beitrag ist jetzt {status}.', 'success')
return redirect(url_for('forum_post', post_id=post_id))
@app.route('/forum/toggle-lock/<int:post_id>', methods=['POST'])
@login_required
def toggle_lock_post(post_id):
"""Sperrt oder entsperrt einen Beitrag für weitere Antworten"""
# Nur Admins und Moderatoren können Beiträge sperren
if current_user.role not in ['admin', 'moderator']:
flash('Du hast keine Berechtigung, Beiträge zu sperren.', 'error')
return redirect(url_for('forum_post', post_id=post_id))
post = ForumPost.query.get_or_404(post_id)
# Nur Hauptbeiträge können gesperrt werden
if post.parent_id:
flash('Nur Hauptbeiträge können gesperrt werden.', 'error')
return redirect(url_for('forum_post', post_id=post.parent_id))
# Ändere den Status
post.is_locked = not post.is_locked
db.session.commit()
status = 'gesperrt' if post.is_locked else 'entsperrt'
flash(f'Der Beitrag ist jetzt {status}.', 'success')
return redirect(url_for('forum_post', post_id=post_id))
# Fehlerbehandlung # Fehlerbehandlung
@app.errorhandler(404) @app.errorhandler(404)
def not_found(e): def not_found(e):

BIN
backup/archiv_0.1.zip Normal file

Binary file not shown.

Binary file not shown.

17
docker-compose.yml Normal file
View File

@@ -0,0 +1,17 @@
version: '3.9'
services:
web:
build: .
image: systades_app:latest
container_name: systades_app
restart: always
env_file:
- .env
ports:
- "5000:5000"
volumes:
- ./database:/app/database
volumes:
db_data:

1
edit_file Normal file
View File

@@ -0,0 +1 @@

View File

@@ -2,10 +2,12 @@
# Kopiere diese Datei zu .env und passe die Werte an # Kopiere diese Datei zu .env und passe die Werte an
# Flask # Flask
SECRET_KEY=dein-geheimer-schluessel-hier FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=your-secret-key-replace-in-production
# OpenAI API # OpenAI API
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
# Datenbank # Datenbank
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden # Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden

View File

@@ -305,4 +305,41 @@ class Document(db.Model):
file_size = db.Column(db.Integer, nullable=True) file_size = db.Column(db.Integer, nullable=True)
def __repr__(self): def __repr__(self):
return f'<Document {self.title}>' return f'<Document {self.title}>'
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
class ForumCategory(db.Model):
id = db.Column(db.Integer, primary_key=True)
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
# Beziehungen
node = db.relationship('MindMapNode', backref='forum_category')
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<ForumCategory {self.title}>'
# Forum-Beitrag-Modell
class ForumPost(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
is_pinned = db.Column(db.Boolean, default=False)
is_locked = db.Column(db.Boolean, default=False)
view_count = db.Column(db.Integer, default=0)
# Beziehungen
author = db.relationship('User', backref='forum_posts')
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
def __repr__(self):
return f'<ForumPost {self.title}>'

53
start.sh Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env powershell
# Windows PowerShell-Version des Start-Skripts
# Datum: 01.05.2025
# Docker-Status prüfen
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
try {
$status = docker ps -q
if ($LASTEXITCODE -ne 0) {
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
exit 1
}
} catch {
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
Write-Host $_.Exception.Message
exit 1
}
# Alte Container stoppen und entfernen
$containerExists = docker ps -a --filter "name=systades_app" -q
if ($containerExists) {
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
docker rm -f systades_app
}
# Alte Images löschen
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
docker rmi -f systades_app:latest
# Stelle sicher, dass das Datenbankverzeichnis existiert
if (-not (Test-Path "database")) {
New-Item -Path "database" -ItemType Directory -Force
}
# Docker-Compose Setup neu bauen
Write-Host "Baue Container neu..." -ForegroundColor Green
docker-compose build --no-cache
# Docker-Compose neu starten
Write-Host "Starte Container..." -ForegroundColor Green
docker-compose up -d --force-recreate
# Warte kurz und prüfe, ob der Container läuft
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
Start-Sleep -Seconds 3
docker ps | Select-String "systades_app"
# Ausgabe
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
Write-Host "----------------------------------------"
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
Write-Host "----------------------------------------"

View File

@@ -0,0 +1 @@

View File

@@ -40,8 +40,8 @@
--light-bg: #f9fafb; --light-bg: #f9fafb;
--light-text: #1e293b; --light-text: #1e293b;
--light-heading: #0f172a; --light-heading: #0f172a;
--light-primary: #3b82f6; --light-primary: #7c3aed;
--light-primary-hover: #4f46e5; --light-primary-hover: #6d28d9;
--light-secondary: #6b7280; --light-secondary: #6b7280;
--light-border: #e5e7eb; --light-border: #e5e7eb;
--light-card-bg: rgba(255, 255, 255, 0.92); --light-card-bg: rgba(255, 255, 255, 0.92);
@@ -523,4 +523,232 @@ body:not(.dark) .navbar {
background-color: var(--light-navbar-bg); background-color: var(--light-navbar-bg);
box-shadow: var(--light-shadow); box-shadow: var(--light-shadow);
border-bottom: 1px solid var(--light-border); border-bottom: 1px solid var(--light-border);
}
/* Erweiterte Light-Mode-spezifische Stile */
body:not(.dark) .glass-effect {
background-color: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(209, 213, 219, 0.3);
}
body:not(.dark) .card {
background-color: rgba(255, 255, 255, 0.85);
border: 1px solid var(--light-border);
box-shadow: var(--light-shadow);
transition: all 0.3s ease;
}
body:not(.dark) .card:hover {
box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
/* Light Mode Buttons */
body:not(.dark) .btn-primary {
background-color: var(--light-primary);
color: white;
border: none;
transition: all 0.2s ease;
}
body:not(.dark) .btn-primary:hover {
background-color: var(--light-primary-hover);
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2);
}
body:not(.dark) .btn-secondary {
background-color: #f3f4f6;
color: var(--light-text);
border: 1px solid #e5e7eb;
}
body:not(.dark) .btn-secondary:hover {
background-color: #e5e7eb;
}
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;
} }

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -69,8 +69,8 @@
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
<!-- Icons - Self-hosted Font Awesome --> <!-- Font Awesome vom CDN -->
<link href="{{ url_for('static', filename='css/all.min.css') }}" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
<!-- Assistent CSS --> <!-- Assistent CSS -->
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
@@ -111,7 +111,7 @@
<!-- Seitenspezifische Styles --> <!-- Seitenspezifische Styles -->
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
<!-- Custom dark mode styles --> <!-- Custom dark/light mode styles -->
<!-- ► ► FarbToken strikt getrennt ◄ ◄ --> <!-- ► ► FarbToken strikt getrennt ◄ ◄ -->
<style> <style>
/* LightMode */ /* LightMode */
@@ -149,6 +149,39 @@
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; } .glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); } .light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); } .dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
/* Light-Mode spezifische Stile */
body:not(.dark) {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.nav-link-light {
color: var(--text-secondary);
transition: all 0.3s ease;
}
.nav-link-light:hover {
color: var(--text-primary);
background-color: rgba(126, 34, 206, 0.1);
}
.nav-link-light-active {
color: var(--accent-primary);
background-color: rgba(126, 34, 206, 0.15);
font-weight: 500;
}
/* Kartendesign im Light-Mode */
body:not(.dark) .card {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
body:not(.dark) .card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
</style> </style>
</head> </head>
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{ <body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
@@ -158,6 +191,17 @@
showSettingsModal: false, showSettingsModal: false,
init() { init() {
this.initDarkMode();
},
initDarkMode() {
// Lade zuerst den Wert aus dem localStorage (client-seitig)
const storedMode = localStorage.getItem('colorMode');
if (storedMode) {
this.darkMode = storedMode === 'dark';
}
// Dann hole die Server-Einstellung, die Vorrang hat
this.fetchDarkModeFromSession(); this.fetchDarkModeFromSession();
}, },
@@ -167,7 +211,7 @@
.then(data => { .then(data => {
if (data.success) { if (data.success) {
this.darkMode = data.darkMode === 'true'; this.darkMode = data.darkMode === 'true';
document.querySelector('html').classList.toggle('dark', this.darkMode); this.applyDarkMode();
} }
}) })
.catch(error => { .catch(error => {
@@ -175,9 +219,15 @@
}); });
}, },
applyDarkMode() {
document.querySelector('html').classList.toggle('dark', this.darkMode);
document.querySelector('body').classList.toggle('dark', this.darkMode);
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
},
toggleDarkMode() { toggleDarkMode() {
this.darkMode = !this.darkMode; this.darkMode = !this.darkMode;
document.querySelector('html').classList.toggle('dark', this.darkMode); this.applyDarkMode();
fetch('/api/set_dark_mode', { fetch('/api/set_dark_mode', {
method: 'POST', method: 'POST',
@@ -189,7 +239,6 @@
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
document.dispatchEvent(new CustomEvent('darkModeToggled', { document.dispatchEvent(new CustomEvent('darkModeToggled', {
detail: { isDark: this.darkMode } detail: { isDark: this.darkMode }
})); }));
@@ -229,6 +278,13 @@
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'"> : '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap <i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
</a> </a>
<a href="{{ url_for('forum') }}"
class="nav-link flex items-center"
x-bind:class="darkMode
? '{{ 'nav-link-active' if request.endpoint == 'forum' else '' }}'
: '{{ 'nav-link-light-active' if request.endpoint == 'forum' else 'nav-link-light' }}'">
<i class="fa-solid fa-users mr-2"></i>Community
</a>
<a href="{{ url_for('search_thoughts_page') }}" <a href="{{ url_for('search_thoughts_page') }}"
class="nav-link flex items-center" class="nav-link flex items-center"
x-bind:class="darkMode x-bind:class="darkMode
@@ -400,6 +456,13 @@
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'"> : '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap <i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
</a> </a>
<a href="{{ url_for('forum') }}"
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
x-bind:class="darkMode
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'forum' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'forum' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
<i class="fa-solid fa-users w-5 mr-3"></i>Community
</a>
<a href="{{ url_for('search_thoughts_page') }}" <a href="{{ url_for('search_thoughts_page') }}"
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center" class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
x-bind:class="darkMode x-bind:class="darkMode
@@ -478,6 +541,10 @@
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'"> :class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Mindmap Mindmap
</a> </a>
<a href="{{ url_for('forum') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Community
</a>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200" <a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'"> :class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
@@ -578,37 +645,42 @@
}); });
</script> </script>
<!-- Dark/Light-Mode persistent und robust --> <!-- Dark/Light-Mode vereinheitlicht -->
<script> <script>
(function() { // Globaler Zugriff für externe Skripte
function applyMode(mode) { window.MindMap = window.MindMap || {};
if (mode === 'dark') {
window.MindMap.toggleDarkMode = function() {
// Alpine.js-Instanz benutzen, wenn verfügbar
const appEl = document.querySelector('body');
if (appEl && appEl.__x) {
appEl.__x.$data.toggleDarkMode();
} else {
// Fallback: Nur classList und localStorage
const isDark = document.documentElement.classList.contains('dark');
document.documentElement.classList.toggle('dark', !isDark);
document.body.classList.toggle('dark', !isDark);
localStorage.setItem('colorMode', !isDark ? 'dark' : 'light');
// Server aktualisieren
fetch('/api/set_dark_mode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ darkMode: !isDark })
}).catch(console.error);
}
};
// Fallback für Browser-Präferenz, falls keine Einstellung geladen werden konnte
document.addEventListener('DOMContentLoaded', function() {
if (!document.body.classList.contains('dark') && !document.documentElement.classList.contains('dark')) {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
localStorage.setItem('colorMode', 'dark'); document.body.classList.add('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> </script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,192 @@
{% extends 'base.html' %}
{% block title %}{{ category.title }} - Forum{% endblock %}
{% block extra_css %}
<style>
.thread-item {
transition: all 0.2s ease;
}
.thread-item:hover {
transform: translateX(2px);
}
.thread-pinned {
border-left-width: 4px;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium">{{ category.title }}</span>
</div>
<!-- Kategorie-Header -->
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center">
<!-- Kategorie-Icon -->
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
style="background-color: {{ node.color_code or '#6d28d9' }}">
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
</div>
<!-- Kategorie-Info -->
<div>
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
<p class="opacity-75">{{ category.description }}</p>
</div>
</div>
<!-- Neues Thema erstellen -->
<a href="{{ url_for('new_post', category_id=category.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-plus-circle mr-2"></i>
Neues Thema
</a>
</div>
<!-- Threads anzeigen -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<!-- Header -->
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="grid grid-cols-12 gap-4">
<div class="col-span-7 font-medium">Thema</div>
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
</div>
</div>
<!-- Thread-Liste -->
{% if threads_data %}
{% for thread_data in threads_data %}
{% set thread = thread_data.thread %}
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
x-bind:class="darkMode
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
<div class="grid grid-cols-12 gap-4">
<!-- Thema -->
<div class="col-span-12 md:col-span-7">
<div class="flex items-start">
<!-- Status-Icons -->
<div class="flex flex-col items-center mr-3 pt-1">
{% if thread.is_pinned %}
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
{% endif %}
{% if thread.is_locked %}
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
{% endif %}
</div>
<!-- Themen-Info -->
<div>
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
{{ thread.title }}
</h3>
<div class="flex items-center text-xs opacity-70 mt-1">
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
<span class="mx-2 block md:hidden"></span>
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
<span class="mx-2"></span>
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
</div>
</div>
</div>
</div>
<!-- Antworten -->
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
x-bind:class="darkMode
? 'bg-indigo-900/40 text-indigo-300'
: 'bg-indigo-100 text-indigo-800'">
{{ thread_data.reply_count }}
</span>
</div>
<!-- Autor -->
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
<div class="flex items-center">
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if thread.author.avatar %}
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
{% else %}
{{ thread.author.username[0].upper() }}
{% endif %}
</div>
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
</div>
</div>
<!-- Letzte Antwort -->
<div class="col-span-2 text-center hidden md:block text-sm">
{% if thread_data.latest_reply %}
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
{% else %}
<span class="opacity-60">Keine Antworten</span>
{% endif %}
</div>
</div>
</a>
</div>
{% endfor %}
{% else %}
<div class="p-8 text-center">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
<a href="{{ url_for('new_post', category_id=category.id) }}"
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-plus-circle mr-2"></i>
Erstes Thema erstellen
</a>
</div>
{% endif %}
</div>
<!-- Link zur Mindmap -->
<div class="rounded-xl p-5 mb-4 flex items-center"
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
<div class="text-3xl mr-4 opacity-80">
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
</div>
<div>
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
</div>
<div class="ml-auto">
<a href="{{ url_for('mindmap') }}"
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
x-bind:class="darkMode
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
Zur Mindmap
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -0,0 +1,344 @@
{% extends 'base.html' %}
{% block title %}Beitrag bearbeiten{% endblock %}
{% block extra_css %}
<style>
.markdown-preview {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
}
.markdown-preview p {
margin-bottom: 1rem;
}
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.markdown-preview h1 { font-size: 1.8rem; }
.markdown-preview h2 { font-size: 1.5rem; }
.markdown-preview h3 { font-size: 1.3rem; }
.markdown-preview h4 { font-size: 1.1rem; }
.markdown-preview ul, .markdown-preview ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-preview ul { list-style-type: disc; }
.markdown-preview ol { list-style-type: decimal; }
.markdown-preview pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.markdown-preview code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.markdown-preview pre code {
padding: 0;
background-color: transparent;
}
.markdown-preview blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.dark .markdown-preview code {
background-color: rgba(255,255,255,0.1);
}
.dark .markdown-preview blockquote {
border-color: rgba(255,255,255,0.2);
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=post.category_id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ post.category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
{% if post.parent_id %}
<a href="{{ url_for('forum_post', post_id=post.parent_id) }}" class="opacity-75 hover:opacity-100 transition-opacity truncate max-w-[200px]">
{{ post.parent.title }}
</a>
<span class="mx-2 opacity-50">/</span>
{% endif %}
<span class="font-medium">Beitrag bearbeiten</span>
</div>
<!-- Formular-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">Beitrag bearbeiten</h1>
<p class="opacity-75">
{% if post.parent_id %}
Antwort auf <span class="font-medium">{{ post.parent.title }}</span>
{% else %}
in der Kategorie <span class="font-medium">{{ post.category.title }}</span>
{% endif %}
</p>
</div>
<!-- Formular -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-2"></i>
Beitrag bearbeiten
</div>
<div class="p-6">
<form action="{{ url_for('edit_post', post_id=post.id) }}" method="POST" x-data="{
title: '{{ post.title|safe }}',
content: '{{ post.content|replace('\n', '\\n')|replace('\'', '\\\'')|safe }}',
showPreview: false,
previewHtml: '',
updatePreview() {
// Verarbeite den Inhalt
if (this.content.trim() === '') {
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
return;
}
// Verarbeite Markdown
let html = marked.parse(this.content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
this.previewHtml = html;
}
}">
<div class="mb-6">
<label for="title" class="block mb-2 font-medium">Titel</label>
<div class="rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<input type="text" id="title" name="title"
class="w-full px-4 py-3"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
x-model="title"
required>
</div>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<label for="content" class="font-medium">Inhalt</label>
<div class="flex space-x-2">
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="showPreview = false"
x-bind:disabled="!showPreview"
x-bind:class="{'opacity-50': !showPreview}">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</button>
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="updatePreview(); showPreview = true"
x-bind:disabled="showPreview"
x-bind:class="{'opacity-50': showPreview}">
<i class="fas fa-eye mr-1"></i> Vorschau
</button>
</div>
</div>
<!-- Editor -->
<div class="rounded-lg overflow-hidden mb-2"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
x-show="!showPreview">
<textarea id="content" name="content" rows="12"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
x-model="content"
required></textarea>
</div>
<!-- Preview -->
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
x-bind:class="darkMode
? 'border border-white/20 bg-gray-700/30'
: 'border border-gray-300 bg-gray-50'"
x-show="showPreview"
x-html="previewHtml">
</div>
<!-- Markdown-Hilfsmittel -->
<div class="mb-4" x-show="!showPreview">
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<a href="{{ url_for('forum_post', post_id=post.parent_id or post.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
Abbrechen
</a>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-save mr-2"></i>
Änderungen speichern
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown-Buttons für den Beitragseditor
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,125 @@
{% extends 'base.html' %}
{% block title %}Community Forum{% endblock %}
{% block extra_css %}
<style>
.forum-category {
transition: all 0.3s ease;
}
.forum-category:hover {
transform: translateY(-2px);
}
.category-icon {
font-size: 1.5rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Seitenüberschrift -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
</div>
<!-- Forumskategorien -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{% if categories_data %}
{% for cat_data in categories_data %}
<a href="{{ url_for('forum_category', category_id=cat_data.category.id) }}" class="forum-category block">
<div class="rounded-xl p-5 h-full"
x-bind:class="darkMode ? 'bg-gray-800/60 hover:bg-gray-800/80 border border-white/10' : 'bg-white hover:bg-gray-50 border border-gray-200 shadow-md'">
<div class="flex items-start">
<!-- Kategorie-Icon -->
<div class="category-icon mr-4 text-white"
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
</div>
<!-- Kategorie-Info -->
<div class="flex-grow">
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
<!-- Statistik -->
<div class="flex flex-wrap gap-4 text-sm opacity-80">
<div class="flex items-center">
<i class="fas fa-comment-alt mr-2"></i>
<span>{{ cat_data.total_posts }} Themen</span>
</div>
<div class="flex items-center">
<i class="fas fa-reply mr-2"></i>
<span>{{ cat_data.total_replies }} Antworten</span>
</div>
{% if cat_data.latest_post %}
<div class="flex items-center">
<i class="fas fa-clock mr-2"></i>
<span>Neuster Beitrag: {{ cat_data.latest_post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Pfeil-Icon -->
<div class="ml-2">
<i class="fas fa-chevron-right opacity-50"></i>
</div>
</div>
</div>
</a>
{% endfor %}
{% else %}
<div class="col-span-2 text-center py-8">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
</div>
{% endif %}
</div>
<!-- Hinweis zur Nutzung -->
<div class="rounded-xl p-6 text-center mb-8"
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
So funktioniert das Forum
</h3>
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Markdown Support</h4>
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -0,0 +1,355 @@
{% extends 'base.html' %}
{% block title %}Neues Thema - {{ category.title }}{% endblock %}
{% block extra_css %}
<style>
.markdown-preview {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
}
.markdown-preview p {
margin-bottom: 1rem;
}
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.markdown-preview h1 { font-size: 1.8rem; }
.markdown-preview h2 { font-size: 1.5rem; }
.markdown-preview h3 { font-size: 1.3rem; }
.markdown-preview h4 { font-size: 1.1rem; }
.markdown-preview ul, .markdown-preview ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-preview ul { list-style-type: disc; }
.markdown-preview ol { list-style-type: decimal; }
.markdown-preview pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.markdown-preview code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.markdown-preview pre code {
padding: 0;
background-color: transparent;
}
.markdown-preview blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.dark .markdown-preview code {
background-color: rgba(255,255,255,0.1);
}
.dark .markdown-preview blockquote {
border-color: rgba(255,255,255,0.2);
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium">Neues Thema</span>
</div>
<!-- Formular-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">Neues Thema erstellen</h1>
<p class="opacity-75">in der Kategorie <span class="font-medium">{{ category.title }}</span></p>
</div>
<!-- Formular -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-plus-circle mr-2"></i>
Neues Thema
</div>
<div class="p-6">
<form action="{{ url_for('new_post', category_id=category.id) }}" method="POST" x-data="{
title: '',
content: '',
showPreview: false,
previewHtml: '',
updatePreview() {
// Verarbeite den Inhalt
if (this.content.trim() === '') {
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
return;
}
// Verarbeite Markdown
let html = marked.parse(this.content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
this.previewHtml = html;
}
}">
<div class="mb-6">
<label for="title" class="block mb-2 font-medium">Titel des Themas</label>
<div class="rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<input type="text" id="title" name="title"
class="w-full px-4 py-3"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Ein prägnanter Titel für dein Thema"
x-model="title"
required>
</div>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<label for="content" class="font-medium">Inhalt</label>
<div class="flex space-x-2">
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="showPreview = false"
x-bind:disabled="!showPreview"
x-bind:class="{'opacity-50': !showPreview}">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</button>
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="updatePreview(); showPreview = true"
x-bind:disabled="showPreview"
x-bind:class="{'opacity-50': showPreview}">
<i class="fas fa-eye mr-1"></i> Vorschau
</button>
</div>
</div>
<!-- Editor -->
<div class="rounded-lg overflow-hidden mb-2"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
x-show="!showPreview">
<textarea id="content" name="content" rows="12"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Schreibe deinen Beitrag hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
x-model="content"
required></textarea>
</div>
<!-- Preview -->
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
x-bind:class="darkMode
? 'border border-white/20 bg-gray-700/30'
: 'border border-gray-300 bg-gray-50'"
x-show="showPreview"
x-html="previewHtml">
</div>
<!-- Markdown-Hilfsmittel -->
<div class="mb-4" x-show="!showPreview">
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<a href="{{ url_for('forum_category', category_id=category.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
Abbrechen
</a>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-paper-plane mr-2"></i>
Thema erstellen
</button>
</div>
</form>
</div>
</div>
<!-- Link zur Mindmap -->
<div class="rounded-xl p-5 mb-4 flex items-center"
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
<div class="text-3xl mr-4 opacity-80">
<i class="fas fa-diagram-project" style="color: {{ category.node.color_code }}"></i>
</div>
<div>
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ category.node.name }}</h3>
<p class="text-sm opacity-75">Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.</p>
</div>
<div class="ml-auto">
<a href="{{ url_for('mindmap') }}"
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
x-bind:class="darkMode
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
Zur Mindmap
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown-Buttons für den Beitragseditor
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,491 @@
{% 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 %}

View File

@@ -0,0 +1,137 @@
{% extends 'base.html' %}
{% block title %}Community Forum Vorschau{% endblock %}
{% block extra_css %}
<style>
.forum-category {
transition: all 0.3s ease;
}
.forum-category:hover {
transform: translateY(-2px);
}
.category-icon {
font-size: 1.5rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Seitenüberschrift -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
</div>
<!-- Login-Aufforderung -->
<div class="rounded-xl p-6 text-center mb-8 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-100 dark:border-indigo-700/30">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lock mr-2 text-indigo-500"></i>
Anmeldung erforderlich
</h3>
<p class="mb-4">Um am Community-Forum teilzunehmen und alle Funktionen nutzen zu können, musst du dich anmelden oder registrieren.</p>
<div class="flex justify-center gap-4 mt-4">
<a href="{{ url_for('login', next=url_for('forum')) }}" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg transition-colors">
<i class="fas fa-sign-in-alt mr-2"></i>Anmelden
</a>
<a href="{{ url_for('register') }}" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg transition-colors">
<i class="fas fa-user-plus mr-2"></i>Registrieren
</a>
</div>
</div>
<!-- Forumskategorien Vorschau -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{% if categories_data %}
{% for cat_data in categories_data %}
<div class="forum-category block">
<div class="rounded-xl p-5 h-full"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-md'">
<div class="flex items-start">
<!-- Kategorie-Icon -->
<div class="category-icon mr-4 text-white"
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
</div>
<!-- Kategorie-Info -->
<div class="flex-grow">
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
<!-- Statistik -->
<div class="flex flex-wrap gap-4 text-sm opacity-80">
<div class="flex items-center">
<i class="fas fa-comment-alt mr-2"></i>
<span>{{ cat_data.total_posts }} Themen</span>
</div>
<div class="flex items-center">
<i class="fas fa-reply mr-2"></i>
<span>{{ cat_data.total_replies }} Antworten</span>
</div>
</div>
</div>
<!-- Pfeil-Icon -->
<div class="ml-2">
<i class="fas fa-lock opacity-50"></i>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-span-2 text-center py-8">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
</div>
{% endif %}
</div>
<!-- Hinweis zur Nutzung -->
<div class="rounded-xl p-6 text-center mb-8"
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
So funktioniert das Forum
</h3>
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Markdown Support</h4>
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -1,234 +1,265 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="de">
<head> {% block title %}Mindmap{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block extra_css %}
<title>Interaktive Mindmap</title> <style>
/* Spezifische Stile für die Mindmap-Seite */
#cy {
width: 100%;
height: 600px;
background-color: var(--bg-secondary);
transition: background-color 0.3s ease;
border-radius: 10px;
overflow: hidden;
position: relative;
}
<!-- Cytoscape.js --> .mindmap-container {
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script> position: relative;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
<!-- Socket.IO --> .dark .mindmap-container {
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script> box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
<!-- Feather Icons (optional) --> .mindmap-toolbar {
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.75rem;
background-color: var(--bg-secondary);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background-color 0.3s ease;
}
<style> body:not(.dark) .mindmap-toolbar {
* { border-bottom: 1px solid rgba(0, 0, 0, 0.1);
box-sizing: border-box; }
margin: 0;
padding: 0; .btn {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
border-radius: 0.375rem;
transition: all 0.3s ease;
}
.category-filters {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.75rem;
background-color: var(--bg-secondary);
transition: background-color 0.3s ease;
}
.category-filter {
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
cursor: pointer;
transition: opacity 0.2s;
color: white;
}
.category-filter:not(.active) {
opacity: 0.6;
}
.category-filter:hover:not(.active) {
opacity: 0.8;
}
/* Kontextmenü */
#context-menu {
position: absolute;
border-radius: 0.375rem;
z-index: 1000;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.dark #context-menu {
background-color: #232837;
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) #context-menu {
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.1);
}
#context-menu .menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
}
.dark #context-menu .menu-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
body:not(.dark) #context-menu .menu-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Zusätzliches Layout */
.mindmap-section {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 1024px) {
.mindmap-section {
grid-template-columns: 2fr 1fr;
} }
}
body { </style>
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; {% endblock %}
background-color: #f9fafb;
color: #111827; {% block content %}
line-height: 1.5; <div class="container mx-auto px-4 py-8">
} <div class="flex flex-col lg:flex-row gap-8">
<!-- Hauptinhalt -->
.container { <div class="w-full lg:w-3/4">
display: flex; <!-- Mindmap-Titelbereich -->
flex-direction: column; <div class="mb-6">
height: 100vh; <h1 class="text-3xl font-bold mb-2 mystical-glow"
width: 100%; x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
} Wissenslandkarte
</h1>
.header { <p class="opacity-80 text-lg"
background-color: #1f2937; x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
color: white; Visualisiere die Verbindungen zwischen Gedanken und Konzepten
padding: 1rem; </p>
display: flex; </div>
justify-content: space-between;
align-items: center; <!-- Mindmap-Container -->
} <div class="mindmap-container">
<!-- Toolbar -->
.header h1 { <div class="mindmap-toolbar">
font-size: 1.5rem; <button id="fit-btn" class="mindmap-action-btn">
font-weight: 500; <i class="fa-solid fa-expand"></i>
} <span>Ansicht anpassen</span>
</button>
.toolbar { <button id="reset-btn" class="mindmap-action-btn">
background-color: #f3f4f6; <i class="fa-solid fa-undo"></i>
padding: 0.75rem; <span>Zurücksetzen</span>
display: flex; </button>
gap: 0.5rem; <button id="toggle-labels-btn" class="mindmap-action-btn">
border-bottom: 1px solid #e5e7eb; <i class="fa-solid fa-tags"></i>
} <span>Labels ein/aus</span>
</button>
.btn { </div>
background-color: #3b82f6;
color: white; <!-- Hauptvisualisierung -->
border: none; <div id="cy"></div>
border-radius: 0.25rem;
padding: 0.5rem 1rem; <!-- Info-Panel -->
font-size: 0.875rem; <div id="node-info-panel" class="mindmap-info-panel">
cursor: pointer; <h4 class="info-panel-title">Knoteninfo</h4>
transition: background-color 0.2s; <p id="node-description" class="info-panel-description">Wählen Sie einen Knoten aus...</p>
display: flex;
align-items: center; <div class="node-navigation">
gap: 0.5rem; <h5 class="node-navigation-title">Verknüpfte Knoten</h5>
} <div id="connected-nodes" class="node-links">
<!-- Wird dynamisch befüllt -->
.btn:hover { </div>
background-color: #2563eb; </div>
} </div>
.btn-secondary {
background-color: #6b7280;
}
.btn-secondary:hover {
background-color: #4b5563;
}
.btn-danger {
background-color: #ef4444;
}
.btn-danger:hover {
background-color: #dc2626;
}
.search-container {
flex: 1;
display: flex;
margin-left: 1rem;
}
.search-input {
width: 100%;
max-width: 300px;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.875rem;
}
#cy {
flex: 1;
width: 100%;
position: relative;
}
.category-filters {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.75rem;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.category-filter {
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
cursor: pointer;
transition: opacity 0.2s;
color: white;
}
.category-filter:not(.active) {
opacity: 0.6;
}
.category-filter:hover:not(.active) {
opacity: 0.8;
}
.footer {
background-color: #f3f4f6;
padding: 0.75rem;
text-align: center;
font-size: 0.75rem;
color: #6b7280;
border-top: 1px solid #e5e7eb;
}
/* Kontextmenü Styling */
#context-menu {
position: absolute;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 0.25rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
#context-menu .menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
}
#context-menu .menu-item:hover {
background-color: #f3f4f6;
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>Interaktive Mindmap</h1>
<div class="search-container">
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
</div> </div>
</header>
<div class="toolbar">
<button id="addNode" class="btn">
<i data-feather="plus-circle"></i>
Knoten hinzufügen
</button>
<button id="addEdge" class="btn">
<i data-feather="git-branch"></i>
Verbindung erstellen
</button>
<button id="editNode" class="btn btn-secondary">
<i data-feather="edit-2"></i>
Knoten bearbeiten
</button>
<button id="deleteNode" class="btn btn-danger">
<i data-feather="trash-2"></i>
Knoten löschen
</button>
<button id="deleteEdge" class="btn btn-danger">
<i data-feather="scissors"></i>
Verbindung löschen
</button>
<button id="reLayout" class="btn btn-secondary">
<i data-feather="refresh-cw"></i>
Layout neu anordnen
</button>
<button id="exportMindmap" class="btn btn-secondary">
<i data-feather="download"></i>
Exportieren
</button>
</div> </div>
<div id="category-filters" class="category-filters"> <!-- Seitenleiste -->
<!-- Wird dynamisch befüllt --> <div class="w-full lg:w-1/4 space-y-6">
<!-- Nutzlänge -->
<div class="p-5 rounded-lg overflow-hidden border transition-colors duration-300"
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
<h3 class="text-xl font-semibold mb-3"
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
<i class="fa-solid fa-circle-info text-purple-400 mr-2"></i>Über die Mindmap
</h3>
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<p class="mb-2">Die interaktive Wissenslandkarte zeigt Verbindungen zwischen verschiedenen Gedanken und Konzepten.</p>
<p class="mb-2">Sie können:</p>
<ul class="list-disc pl-5 space-y-1 text-sm">
<li>Knoten auswählen, um Details zu sehen</li>
<li>Zoomen (Mausrad oder Pinch-Geste)</li>
<li>Die Karte verschieben (Drag & Drop)</li>
<li>Die Toolbar nutzen für weitere Aktionen</li>
</ul>
</div>
</div>
<!-- Kategorienlegende -->
<div class="p-5 rounded-lg overflow-hidden border transition-colors duration-300"
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
<h3 class="text-xl font-semibold mb-3"
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
<i class="fa-solid fa-palette text-purple-400 mr-2"></i>Kategorien
</h3>
<div id="category-legend" class="space-y-2 text-sm"
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<!-- Wird dynamisch befüllt -->
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-purple-500 mr-2"></span> Philosophie</div>
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-green-500 mr-2"></span> Wissenschaft</div>
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-orange-500 mr-2"></span> Technologie</div>
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-pink-500 mr-2"></span> Künste</div>
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-blue-500 mr-2"></span> Psychologie</div>
</div>
</div>
<!-- Meine Mindmaps -->
{% if current_user.is_authenticated %}
<div class="p-5 rounded-lg overflow-hidden border transition-colors duration-300"
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
<h3 class="text-xl font-semibold mb-3"
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
<i class="fa-solid fa-map text-purple-400 mr-2"></i>Meine Mindmaps
</h3>
<div class="mb-3">
<a href="{{ url_for('create_mindmap') }}" class="w-full inline-block py-2 px-4 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-center text-sm font-medium transition-colors">
<i class="fa-solid fa-plus mr-1"></i> Neue Mindmap erstellen
</a>
</div>
<div class="space-y-2 max-h-60 overflow-y-auto"
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
{% if user_mindmaps %}
{% for mindmap in user_mindmaps %}
<a href="{{ url_for('user_mindmap', mindmap_id=mindmap.id) }}" class="block p-2 hover:bg-purple-500/20 rounded-lg transition-colors">
<div class="text-sm font-medium">{{ mindmap.name }}</div>
<div class="text-xs opacity-70">{{ mindmap.nodes|length }} Knoten</div>
</a>
{% endfor %}
{% else %}
<p class="text-sm italic">Sie haben noch keine eigenen Mindmaps erstellt.</p>
{% endif %}
</div>
</div>
{% endif %}
</div> </div>
<div id="cy"></div>
<footer class="footer">
Mindmap-Anwendung © 2023
</footer>
</div> </div>
</div>
<!-- Unsere Mindmap JS --> {% endblock %}
<script src="{{ url_for('static', filename='js/mindmap.js') }}"></script>
{% block extra_js %}
<!-- Icons initialisieren --> <script>
<script> // Sobald die Seite und die Scripte geladen sind, initialisiere die Mindmap
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', function() {
if (typeof feather !== 'undefined') { if (window.MindMap && window.MindMap.pageInitializers && window.MindMap.pageInitializers.mindmap) {
feather.replace(); window.MindMap.pageInitializers.mindmap();
} } else {
}); console.error('Mindmap-Initialisierung konnte nicht gefunden werden!');
</script> }
</body> });
</html> </script>
{% endblock %}

View File

@@ -1,5 +1,5 @@
home = C:\Program Files\Python313 home = C:\Program Files\Python313
include-system-site-packages = false include-system-site-packages = false
version = 3.13.3 version = 3.13.3
executable = C:\Program Files\Python313\python.exe executable = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe
command = C:\Program Files\Python313\python.exe -m venv C:\Users\TTOMCZA.EMEA\Dev\website\venv command = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe -m venv C:\Users\firem\Desktop\111\Systades\website\venv

View File

@@ -1,58 +0,0 @@
@echo off
echo Mindmap Projekt - Windows Setup
echo ==============================
echo.
REM Prüfen, ob Python installiert ist
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo Python ist nicht installiert oder nicht im PATH.
echo Bitte installiere Python 3.11 von https://www.python.org/downloads/
echo und stelle sicher, dass "Add Python to PATH" während der Installation aktiviert ist.
pause
exit /b 1
)
echo Erstelle virtuelle Umgebung...
python -m venv venv
if %errorlevel% neq 0 (
echo Fehler beim Erstellen der virtuellen Umgebung.
pause
exit /b 1
)
echo Aktiviere virtuelle Umgebung...
call venv\Scripts\activate.bat
if %errorlevel% neq 0 (
echo Fehler beim Aktivieren der virtuellen Umgebung.
pause
exit /b 1
)
echo Aktualisiere pip...
python -m pip install --upgrade pip
if %errorlevel% neq 0 (
echo Warnung: Pip konnte nicht aktualisiert werden. Fahre trotzdem fort.
)
echo Installiere Abhängigkeiten...
pip install -r requirements.txt
if %errorlevel% neq 0 (
echo Fehler beim Installieren der Abhängigkeiten.
pause
exit /b 1
)
echo.
echo Setup abgeschlossen!
echo.
echo Zum Starten des Servers:
echo 1. Führe "venv\Scripts\activate.bat" aus
echo 2. Führe "python TOOLS.py db:rebuild" aus (Nur beim ersten Mal oder zum Zurücksetzen der Datenbank)
echo 3. Führe "python TOOLS.py user:admin" aus (Erstellt einen Admin-Benutzer: admin/admin)
echo 4. Führe "python TOOLS.py server:run" aus
echo.
echo Die Anwendung ist dann unter http://localhost:5000 erreichbar.
echo.
pause