Compare commits
69 Commits
613c38ccb2
...
tills-bran
| Author | SHA1 | Date | |
|---|---|---|---|
| a7bb9563b3 | |||
| 4e0c470663 | |||
| b5300f74bd | |||
| 6a53e621ca | |||
| a59ce652af | |||
| 27cfc95081 | |||
| c513666391 | |||
| ae30dbce57 | |||
| 817ddd98e9 | |||
| bfce2fc7b7 | |||
| efbcd567ee | |||
| a873765d08 | |||
| efbcadb95a | |||
| da3ccaffe9 | |||
| f4e04573bd | |||
| aa253f3871 | |||
| cfd6a25b21 | |||
| d307763007 | |||
| d7e6912e08 | |||
| ffe96074f4 | |||
| 49ccf3908a | |||
| 9514645904 | |||
| 63f45abb3e | |||
| 7d74b5a7bf | |||
| 55f2553780 | |||
| 0852ea070b | |||
| 7a0533ac09 | |||
| 65c44ab371 | |||
| 5399169b11 | |||
| 05f6f149ad | |||
| 12ed413c04 | |||
| ccf5a1d678 | |||
| eb23d638e6 | |||
| 750dba2d6f | |||
| 6aa3780a96 | |||
| 8890a62026 | |||
| 6cf9b2a627 | |||
| 4f6aea8e20 | |||
| e5f485d9d7 | |||
| cf3fc09a63 | |||
| 10747a8336 | |||
| 7eb958f3c8 | |||
| 4a3092a4d2 | |||
| 2d8cdc052f | |||
| 968515ce2b | |||
| 88f8e98df0 | |||
| e5409eef68 | |||
| 013bf76446 | |||
| 808a3c7bbe | |||
| d117978005 | |||
| 48d8463481 | |||
| 08314ec703 | |||
| 0bb7d8d0dc | |||
| 4a28c2c453 | |||
| 66d987857a | |||
| d58aba26c2 | |||
| 8f0a6d4372 | |||
| 5372fe220e | |||
| 11ab15127c | |||
| 0705ecce59 | |||
| 1c59b0b616 | |||
| d42c43db50 | |||
| e46264b201 | |||
| 74307ba345 | |||
| 14474c4eab | |||
| 4797cc3b72 | |||
| ab280b55af | |||
| 84b492d8d2 | |||
| b0db3398f2 |
17
.env
17
.env
@@ -1,15 +1,2 @@
|
|||||||
# MindMap Umgebungsvariablen
|
SECRET_KEY=eed9298856dc9363cd32778265780d6904ba24e6a6b815a2cc382bcdd767ea7b
|
||||||
# Kopiere diese Datei zu .env und passe die Werte an
|
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||||
|
|
||||||
# Flask
|
|
||||||
FLASK_APP=app.py
|
|
||||||
FLASK_ENV=development
|
|
||||||
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
33
Dockerfile
@@ -1,33 +0,0 @@
|
|||||||
# Dockerfile
|
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
# Arbeitsverzeichnis in Container
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends gcc && \
|
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
|
||||||
mkdir -p /app/database
|
|
||||||
|
|
||||||
# pip auf den neuesten Stand bringen und Requirements installieren
|
|
||||||
COPY requirements.txt ./
|
|
||||||
RUN pip install --upgrade pip && \
|
|
||||||
pip install --no-cache-dir -U -r requirements.txt
|
|
||||||
|
|
||||||
# Anwendungscode kopieren
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Berechtigungen für database-Ordner
|
|
||||||
RUN chmod -R 777 /app/database
|
|
||||||
|
|
||||||
# Exponiere Port 5000 für Flask
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Setze Umgebungsvariablen
|
|
||||||
ENV FLASK_APP=app.py
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
|
||||||
|
|
||||||
# Startkommando mit spezifischen Flags für Produktion
|
|
||||||
CMD ["python", "app.py"]
|
|
||||||
Binary file not shown.
Binary file not shown.
377
app.py
377
app.py
@@ -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, ForumCategory, ForumPost
|
node_thought_association, user_thought_bookmark, node_relationship
|
||||||
)
|
)
|
||||||
|
|
||||||
# Lade .env-Datei
|
# Lade .env-Datei
|
||||||
@@ -190,31 +190,6 @@ 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:
|
||||||
@@ -223,34 +198,97 @@ def initialize_database():
|
|||||||
# Erstelle alle Tabellen
|
# Erstelle alle Tabellen
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
# Prüfen, ob bereits Kategorien existieren
|
# Prüfe, ob bereits Benutzer existieren
|
||||||
categories_count = Category.query.count()
|
if User.query.count() == 0:
|
||||||
users_count = User.query.count()
|
print("Erstelle Admin-Benutzer...")
|
||||||
|
admin = User(
|
||||||
# Erstelle Standarddaten, wenn es keine Kategorien gibt
|
|
||||||
if categories_count == 0:
|
|
||||||
create_default_categories()
|
|
||||||
|
|
||||||
# Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind
|
|
||||||
if users_count == 0:
|
|
||||||
admin_user = User(
|
|
||||||
username="admin",
|
username="admin",
|
||||||
email="admin@example.com",
|
email="admin@example.com",
|
||||||
role="admin",
|
is_admin=True
|
||||||
is_active=True
|
|
||||||
)
|
)
|
||||||
admin_user.set_password("admin123") # Sicheres Passwort in der Produktion verwenden!
|
admin.set_password("admin123") # In echter Umgebung ein sicheres Passwort verwenden!
|
||||||
db.session.add(admin_user)
|
db.session.add(admin)
|
||||||
|
|
||||||
|
# Prüfe, ob bereits Kategorien existieren
|
||||||
|
if Category.query.count() == 0:
|
||||||
|
print("Erstelle Standard-Kategorien...")
|
||||||
|
create_default_categories()
|
||||||
|
|
||||||
|
# Stelle sicher, dass die Standard-Knoten für die öffentliche Mindmap existieren
|
||||||
|
if MindMapNode.query.count() == 0:
|
||||||
|
print("Erstelle Standard-Knoten für die Mindmap...")
|
||||||
|
|
||||||
|
# Hauptknoten: Wissen
|
||||||
|
root_node = MindMapNode(
|
||||||
|
name="Wissen",
|
||||||
|
description="Zentrale Wissensbasis",
|
||||||
|
color_code="#4299E1",
|
||||||
|
is_public=True
|
||||||
|
)
|
||||||
|
db.session.add(root_node)
|
||||||
|
db.session.flush() # Um die ID zu generieren
|
||||||
|
|
||||||
|
# Verwandte Kategorien finden
|
||||||
|
philosophy = Category.query.filter_by(name="Philosophie").first()
|
||||||
|
science = Category.query.filter_by(name="Wissenschaft").first()
|
||||||
|
technology = Category.query.filter_by(name="Technologie").first()
|
||||||
|
arts = Category.query.filter_by(name="Künste").first()
|
||||||
|
|
||||||
|
# Erstelle Hauptthemenknoten
|
||||||
|
nodes = [
|
||||||
|
MindMapNode(
|
||||||
|
name="Philosophie",
|
||||||
|
description="Philosophisches Denken",
|
||||||
|
color_code="#9F7AEA",
|
||||||
|
category=philosophy,
|
||||||
|
is_public=True
|
||||||
|
),
|
||||||
|
MindMapNode(
|
||||||
|
name="Wissenschaft",
|
||||||
|
description="Wissenschaftliche Erkenntnisse",
|
||||||
|
color_code="#48BB78",
|
||||||
|
category=science,
|
||||||
|
is_public=True
|
||||||
|
),
|
||||||
|
MindMapNode(
|
||||||
|
name="Technologie",
|
||||||
|
description="Technologische Entwicklungen",
|
||||||
|
color_code="#ED8936",
|
||||||
|
category=technology,
|
||||||
|
is_public=True
|
||||||
|
),
|
||||||
|
MindMapNode(
|
||||||
|
name="Künste",
|
||||||
|
description="Künstlerische Ausdrucksformen",
|
||||||
|
color_code="#ED64A6",
|
||||||
|
category=arts,
|
||||||
|
is_public=True
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Füge Knoten zur Datenbank hinzu
|
||||||
|
for node in nodes:
|
||||||
|
db.session.add(node)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print("Admin-Benutzer wurde erstellt!")
|
|
||||||
|
|
||||||
# Forum-Kategorien erstellen
|
# Nachdem wir die IDs haben, füge die Verbindungen hinzu
|
||||||
create_forum_categories()
|
all_nodes = MindMapNode.query.all()
|
||||||
|
root = MindMapNode.query.filter_by(name="Wissen").first()
|
||||||
|
|
||||||
return True
|
if root:
|
||||||
|
for node in all_nodes:
|
||||||
|
if node.id != root.id:
|
||||||
|
root.children.append(node)
|
||||||
|
|
||||||
|
# Speichere die Änderungen
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print("Datenbankinitialisierung abgeschlossen.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fehler bei Datenbank-Initialisierung: {e}")
|
print(f"Fehler bei der Datenbankinitialisierung: {str(e)}")
|
||||||
return False
|
db.session.rollback()
|
||||||
|
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
|
||||||
@@ -1483,251 +1521,6 @@ def refresh_mindmap():
|
|||||||
def mindmap_page():
|
def mindmap_page():
|
||||||
return render_template('mindmap.html')
|
return render_template('mindmap.html')
|
||||||
|
|
||||||
# Community-Forum-Routen
|
|
||||||
@app.route('/community')
|
|
||||||
@login_required
|
|
||||||
def community():
|
|
||||||
"""Hauptseite des Community-Forums"""
|
|
||||||
forum_categories = ForumCategory.query.filter_by(is_active=True).all()
|
|
||||||
|
|
||||||
# Statistiken für jede Kategorie berechnen
|
|
||||||
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('/community/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('/community/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('/community/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('/community/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('/community/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('/community/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('/community/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('/community/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):
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,17 +0,0 @@
|
|||||||
version: '3.9'
|
|
||||||
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
image: systades_app:latest
|
|
||||||
container_name: systades_app
|
|
||||||
restart: always
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
volumes:
|
|
||||||
- ./database:/app/database
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db_data:
|
|
||||||
@@ -2,12 +2,10 @@
|
|||||||
# Kopiere diese Datei zu .env und passe die Werte an
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
FLASK_APP=app.py
|
SECRET_KEY=dein-geheimer-schluessel-hier
|
||||||
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
|
||||||
|
|||||||
37
models.py
37
models.py
@@ -306,40 +306,3 @@ class Document(db.Model):
|
|||||||
|
|
||||||
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
53
start.sh
@@ -1,53 +0,0 @@
|
|||||||
#!/usr/bin/env powershell
|
|
||||||
# Windows PowerShell-Version des Start-Skripts
|
|
||||||
# Datum: 01.05.2025
|
|
||||||
|
|
||||||
# Docker-Status prüfen
|
|
||||||
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
|
|
||||||
try {
|
|
||||||
$status = docker ps -q
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
|
|
||||||
Write-Host $_.Exception.Message
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Alte Container stoppen und entfernen
|
|
||||||
$containerExists = docker ps -a --filter "name=systades_app" -q
|
|
||||||
if ($containerExists) {
|
|
||||||
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
|
|
||||||
docker rm -f systades_app
|
|
||||||
}
|
|
||||||
|
|
||||||
# Alte Images löschen
|
|
||||||
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
|
|
||||||
docker rmi -f systades_app:latest
|
|
||||||
|
|
||||||
# Stelle sicher, dass das Datenbankverzeichnis existiert
|
|
||||||
if (-not (Test-Path "database")) {
|
|
||||||
New-Item -Path "database" -ItemType Directory -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
# Docker-Compose Setup neu bauen
|
|
||||||
Write-Host "Baue Container neu..." -ForegroundColor Green
|
|
||||||
docker-compose build --no-cache
|
|
||||||
|
|
||||||
# Docker-Compose neu starten
|
|
||||||
Write-Host "Starte Container..." -ForegroundColor Green
|
|
||||||
docker-compose up -d --force-recreate
|
|
||||||
|
|
||||||
# Warte kurz und prüfe, ob der Container läuft
|
|
||||||
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
|
|
||||||
Start-Sleep -Seconds 3
|
|
||||||
docker ps | Select-String "systades_app"
|
|
||||||
|
|
||||||
# Ausgabe
|
|
||||||
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
|
|
||||||
Write-Host "----------------------------------------"
|
|
||||||
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
|
|
||||||
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
|
|
||||||
Write-Host "----------------------------------------"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
--light-bg: #f9fafb;
|
--light-bg: #f9fafb;
|
||||||
--light-text: #1e293b;
|
--light-text: #1e293b;
|
||||||
--light-heading: #0f172a;
|
--light-heading: #0f172a;
|
||||||
--light-primary: #7c3aed;
|
--light-primary: #3b82f6;
|
||||||
--light-primary-hover: #6d28d9;
|
--light-primary-hover: #4f46e5;
|
||||||
--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);
|
||||||
@@ -524,231 +524,3 @@ body:not(.dark) .navbar {
|
|||||||
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.
@@ -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">
|
||||||
|
|
||||||
<!-- Font Awesome vom CDN -->
|
<!-- Icons - Self-hosted Font Awesome -->
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
<link href="{{ url_for('static', filename='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/light mode styles -->
|
<!-- Custom dark mode styles -->
|
||||||
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
<!-- ► ► Farb‑Token strikt getrennt ◄ ◄ -->
|
||||||
<style>
|
<style>
|
||||||
/* Light‑Mode */
|
/* Light‑Mode */
|
||||||
@@ -149,39 +149,6 @@
|
|||||||
.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="{
|
||||||
@@ -191,17 +158,6 @@
|
|||||||
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();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -211,7 +167,7 @@
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
this.darkMode = data.darkMode === 'true';
|
this.darkMode = data.darkMode === 'true';
|
||||||
this.applyDarkMode();
|
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -219,15 +175,9 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
applyDarkMode() {
|
|
||||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
|
||||||
document.querySelector('body').classList.toggle('dark', this.darkMode);
|
|
||||||
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleDarkMode() {
|
toggleDarkMode() {
|
||||||
this.darkMode = !this.darkMode;
|
this.darkMode = !this.darkMode;
|
||||||
this.applyDarkMode();
|
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||||
|
|
||||||
fetch('/api/set_dark_mode', {
|
fetch('/api/set_dark_mode', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -239,6 +189,7 @@
|
|||||||
.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 }
|
||||||
}));
|
}));
|
||||||
@@ -278,13 +229,6 @@
|
|||||||
: '{{ '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('community') }}"
|
|
||||||
class="nav-link flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? '{{ 'nav-link-active' if request.endpoint == 'community' else '' }}'
|
|
||||||
: '{{ 'nav-link-light-active' if request.endpoint == 'community' 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
|
||||||
@@ -456,13 +400,6 @@
|
|||||||
: '{{ '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('community') }}"
|
|
||||||
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 == 'community' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
|
||||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'community' 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
|
||||||
@@ -541,10 +478,6 @@
|
|||||||
: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('community') }}" 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'">
|
||||||
@@ -645,42 +578,37 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Dark/Light-Mode vereinheitlicht -->
|
<!-- Dark/Light-Mode persistent und robust -->
|
||||||
<script>
|
<script>
|
||||||
// Globaler Zugriff für externe Skripte
|
(function() {
|
||||||
window.MindMap = window.MindMap || {};
|
function applyMode(mode) {
|
||||||
|
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');
|
||||||
document.body.classList.add('dark');
|
localStorage.setItem('colorMode', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
localStorage.setItem('colorMode', 'light');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
// Beim Laden: Präferenz aus localStorage oder System übernehmen
|
||||||
|
const stored = localStorage.getItem('colorMode');
|
||||||
|
if (stored === 'dark' || stored === 'light') {
|
||||||
|
applyMode(stored);
|
||||||
|
} else {
|
||||||
|
// Systempräferenz als Fallback
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
applyMode(prefersDark ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
// Umschalter für alle Mode-Toggles
|
||||||
|
window.toggleColorMode = function() {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
applyMode(isDark ? 'light' : 'dark');
|
||||||
|
};
|
||||||
|
// Optional: globales Event für andere Skripte
|
||||||
|
window.addEventListener('storage', function(e) {
|
||||||
|
if (e.key === 'colorMode') applyMode(e.newValue);
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}{{ category.title }} - Forum{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<style>
|
|
||||||
.thread-item {
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
.thread-item:hover {
|
|
||||||
transform: translateX(2px);
|
|
||||||
}
|
|
||||||
.thread-pinned {
|
|
||||||
border-left-width: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
|
||||||
<!-- Breadcrumb Navigation -->
|
|
||||||
<div class="mb-6 flex items-center text-sm">
|
|
||||||
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
|
||||||
<i class="fas fa-home mr-1"></i> Forum
|
|
||||||
</a>
|
|
||||||
<span class="mx-2 opacity-50">/</span>
|
|
||||||
<span class="font-medium">{{ category.title }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Kategorie-Header -->
|
|
||||||
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<!-- Kategorie-Icon -->
|
|
||||||
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
|
|
||||||
style="background-color: {{ node.color_code or '#6d28d9' }}">
|
|
||||||
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Kategorie-Info -->
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
|
|
||||||
<p class="opacity-75">{{ category.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Neues Thema erstellen -->
|
|
||||||
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
|
||||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
|
||||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
|
||||||
<i class="fas fa-plus-circle mr-2"></i>
|
|
||||||
Neues Thema
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Threads anzeigen -->
|
|
||||||
<div class="mb-8 rounded-xl overflow-hidden"
|
|
||||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
|
||||||
<div class="grid grid-cols-12 gap-4">
|
|
||||||
<div class="col-span-7 font-medium">Thema</div>
|
|
||||||
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
|
|
||||||
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
|
|
||||||
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Thread-Liste -->
|
|
||||||
{% if threads_data %}
|
|
||||||
{% for thread_data in threads_data %}
|
|
||||||
{% set thread = thread_data.thread %}
|
|
||||||
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
|
|
||||||
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
|
|
||||||
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
|
|
||||||
<div class="grid grid-cols-12 gap-4">
|
|
||||||
<!-- Thema -->
|
|
||||||
<div class="col-span-12 md:col-span-7">
|
|
||||||
<div class="flex items-start">
|
|
||||||
<!-- Status-Icons -->
|
|
||||||
<div class="flex flex-col items-center mr-3 pt-1">
|
|
||||||
{% if thread.is_pinned %}
|
|
||||||
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
|
|
||||||
{% endif %}
|
|
||||||
{% if thread.is_locked %}
|
|
||||||
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Themen-Info -->
|
|
||||||
<div>
|
|
||||||
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
|
|
||||||
{{ thread.title }}
|
|
||||||
</h3>
|
|
||||||
<div class="flex items-center text-xs opacity-70 mt-1">
|
|
||||||
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
|
|
||||||
<span class="mx-2 block md:hidden">•</span>
|
|
||||||
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
|
|
||||||
<span class="mx-2">•</span>
|
|
||||||
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Antworten -->
|
|
||||||
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
|
|
||||||
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-indigo-900/40 text-indigo-300'
|
|
||||||
: 'bg-indigo-100 text-indigo-800'">
|
|
||||||
{{ thread_data.reply_count }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Autor -->
|
|
||||||
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
|
|
||||||
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
|
||||||
{% if thread.author.avatar %}
|
|
||||||
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
|
|
||||||
{% else %}
|
|
||||||
{{ thread.author.username[0].upper() }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Letzte Antwort -->
|
|
||||||
<div class="col-span-2 text-center hidden md:block text-sm">
|
|
||||||
{% if thread_data.latest_reply %}
|
|
||||||
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
|
|
||||||
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
|
|
||||||
{% else %}
|
|
||||||
<span class="opacity-60">Keine Antworten</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<div class="p-8 text-center">
|
|
||||||
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
|
||||||
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
|
|
||||||
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
|
|
||||||
<a href="{{ url_for('new_post', category_id=category.id) }}"
|
|
||||||
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
|
||||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
|
||||||
<i class="fas fa-plus-circle mr-2"></i>
|
|
||||||
Erstes Thema erstellen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Link zur Mindmap -->
|
|
||||||
<div class="rounded-xl p-5 mb-4 flex items-center"
|
|
||||||
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
|
||||||
<div class="text-3xl mr-4 opacity-80">
|
|
||||||
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
|
|
||||||
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
|
|
||||||
</div>
|
|
||||||
<div class="ml-auto">
|
|
||||||
<a href="{{ url_for('mindmap') }}"
|
|
||||||
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
|
||||||
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
|
||||||
Zur Mindmap
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Beitrag bearbeiten{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<style>
|
|
||||||
.markdown-preview {
|
|
||||||
min-height: 200px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.markdown-preview p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
|
||||||
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.markdown-preview h1 { font-size: 1.8rem; }
|
|
||||||
.markdown-preview h2 { font-size: 1.5rem; }
|
|
||||||
.markdown-preview h3 { font-size: 1.3rem; }
|
|
||||||
.markdown-preview h4 { font-size: 1.1rem; }
|
|
||||||
.markdown-preview ul, .markdown-preview ol {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.markdown-preview ul { list-style-type: disc; }
|
|
||||||
.markdown-preview ol { list-style-type: decimal; }
|
|
||||||
.markdown-preview pre {
|
|
||||||
background-color: rgba(0,0,0,0.05);
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
.markdown-preview code {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
padding: 0.1em 0.3em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
background-color: rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
.markdown-preview pre code {
|
|
||||||
padding: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.markdown-preview blockquote {
|
|
||||||
border-left: 4px solid;
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
.dark .markdown-preview code {
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
.dark .markdown-preview blockquote {
|
|
||||||
border-color: rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
.node-mention {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(109, 40, 217, 0.1);
|
|
||||||
color: #6d28d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1px 6px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
margin: 0 2px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.dark .node-mention {
|
|
||||||
background-color: rgba(167, 139, 250, 0.2);
|
|
||||||
color: #a78bfa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
|
||||||
<!-- Breadcrumb Navigation -->
|
|
||||||
<div class="mb-6 flex items-center text-sm">
|
|
||||||
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
|
||||||
<i class="fas fa-home mr-1"></i> Forum
|
|
||||||
</a>
|
|
||||||
<span class="mx-2 opacity-50">/</span>
|
|
||||||
<a href="{{ url_for('forum_category', category_id=post.category_id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
|
||||||
{{ post.category.title }}
|
|
||||||
</a>
|
|
||||||
<span class="mx-2 opacity-50">/</span>
|
|
||||||
{% if post.parent_id %}
|
|
||||||
<a href="{{ url_for('forum_post', post_id=post.parent_id) }}" class="opacity-75 hover:opacity-100 transition-opacity truncate max-w-[200px]">
|
|
||||||
{{ post.parent.title }}
|
|
||||||
</a>
|
|
||||||
<span class="mx-2 opacity-50">/</span>
|
|
||||||
{% endif %}
|
|
||||||
<span class="font-medium">Beitrag bearbeiten</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formular-Header -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<h1 class="text-2xl font-bold mb-2">Beitrag bearbeiten</h1>
|
|
||||||
<p class="opacity-75">
|
|
||||||
{% if post.parent_id %}
|
|
||||||
Antwort auf <span class="font-medium">{{ post.parent.title }}</span>
|
|
||||||
{% else %}
|
|
||||||
in der Kategorie <span class="font-medium">{{ post.category.title }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formular -->
|
|
||||||
<div class="mb-8 rounded-xl overflow-hidden"
|
|
||||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
|
||||||
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
|
||||||
<i class="fas fa-edit mr-2"></i>
|
|
||||||
Beitrag bearbeiten
|
|
||||||
</div>
|
|
||||||
<div class="p-6">
|
|
||||||
<form action="{{ url_for('edit_post', post_id=post.id) }}" method="POST" x-data="{
|
|
||||||
title: '{{ post.title|safe }}',
|
|
||||||
content: '{{ post.content|replace('\n', '\\n')|replace('\'', '\\\'')|safe }}',
|
|
||||||
showPreview: false,
|
|
||||||
previewHtml: '',
|
|
||||||
|
|
||||||
updatePreview() {
|
|
||||||
// Verarbeite den Inhalt
|
|
||||||
if (this.content.trim() === '') {
|
|
||||||
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verarbeite Markdown
|
|
||||||
let html = marked.parse(this.content);
|
|
||||||
|
|
||||||
// Ersetze @Knotenname mit entsprechenden Links
|
|
||||||
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
|
||||||
|
|
||||||
this.previewHtml = html;
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="title" class="block mb-2 font-medium">Titel</label>
|
|
||||||
<div class="rounded-lg overflow-hidden"
|
|
||||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
|
||||||
<input type="text" id="title" name="title"
|
|
||||||
class="w-full px-4 py-3"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
|
||||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
|
||||||
x-model="title"
|
|
||||||
required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<label for="content" class="font-medium">Inhalt</label>
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<button type="button"
|
|
||||||
class="px-3 py-1 rounded text-sm flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
|
||||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
|
||||||
@click="showPreview = false"
|
|
||||||
x-bind:disabled="!showPreview"
|
|
||||||
x-bind:class="{'opacity-50': !showPreview}">
|
|
||||||
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
|
||||||
</button>
|
|
||||||
<button type="button"
|
|
||||||
class="px-3 py-1 rounded text-sm flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
|
||||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
|
||||||
@click="updatePreview(); showPreview = true"
|
|
||||||
x-bind:disabled="showPreview"
|
|
||||||
x-bind:class="{'opacity-50': showPreview}">
|
|
||||||
<i class="fas fa-eye mr-1"></i> Vorschau
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Editor -->
|
|
||||||
<div class="rounded-lg overflow-hidden mb-2"
|
|
||||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
|
||||||
x-show="!showPreview">
|
|
||||||
<textarea id="content" name="content" rows="12"
|
|
||||||
class="w-full p-3 resize-y"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
|
||||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
|
||||||
x-model="content"
|
|
||||||
required></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preview -->
|
|
||||||
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'border border-white/20 bg-gray-700/30'
|
|
||||||
: 'border border-gray-300 bg-gray-50'"
|
|
||||||
x-show="showPreview"
|
|
||||||
x-html="previewHtml">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Markdown-Hilfsmittel -->
|
|
||||||
<div class="mb-4" x-show="!showPreview">
|
|
||||||
<div class="text-xs opacity-70">
|
|
||||||
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
|
||||||
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
|
||||||
<div class="flex flex-wrap gap-2 mt-1">
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
|
||||||
<i class="fas fa-bold"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
|
||||||
<i class="fas fa-italic"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
|
||||||
<i class="fas fa-code"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
|
||||||
<i class="fas fa-link"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
|
||||||
<i class="fas fa-file-code"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
|
||||||
<i class="fas fa-quote-right"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
|
||||||
<i class="fas fa-list-ul"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
|
||||||
<i class="fas fa-list-ol"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
|
||||||
<i class="fas fa-heading"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<a href="{{ url_for('forum_post', post_id=post.parent_id or post.id) }}"
|
|
||||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
|
||||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
|
||||||
Abbrechen
|
|
||||||
</a>
|
|
||||||
<button type="submit"
|
|
||||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
|
||||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
|
||||||
<i class="fas fa-save mr-2"></i>
|
|
||||||
Änderungen speichern
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Markdown-Buttons für den Beitragseditor
|
|
||||||
document.querySelectorAll('.markdown-button').forEach(button => {
|
|
||||||
button.addEventListener('click', function() {
|
|
||||||
const textarea = document.getElementById('content');
|
|
||||||
const format = this.dataset.format;
|
|
||||||
const before = this.dataset.before || '';
|
|
||||||
const after = this.dataset.after || '';
|
|
||||||
|
|
||||||
// Hole die aktuelle Auswahl
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const selection = textarea.value.substring(start, end);
|
|
||||||
|
|
||||||
// Wende die Formatierung an
|
|
||||||
let formattedText;
|
|
||||||
if (format.includes('\n')) {
|
|
||||||
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
|
||||||
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
|
||||||
} else if (format.includes('[Link-Text](URL)')) {
|
|
||||||
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
|
||||||
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
|
||||||
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
|
||||||
const beforeSelection = textarea.value.substring(0, start);
|
|
||||||
const afterSelection = textarea.value.substring(end);
|
|
||||||
|
|
||||||
// Finde den Anfang der aktuellen Zeile
|
|
||||||
const lastNewline = beforeSelection.lastIndexOf('\n');
|
|
||||||
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
|
||||||
|
|
||||||
// Füge das Format am Zeilenanfang ein
|
|
||||||
formattedText = beforeSelection.substring(0, lineStart) +
|
|
||||||
format +
|
|
||||||
beforeSelection.substring(lineStart) +
|
|
||||||
selection +
|
|
||||||
afterSelection;
|
|
||||||
|
|
||||||
// Setze die neue Cursor-Position
|
|
||||||
const newCursorPos = end + format.length;
|
|
||||||
textarea.value = formattedText;
|
|
||||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
||||||
|
|
||||||
// Alpine.js Model aktualisieren
|
|
||||||
textarea.dispatchEvent(new Event('input'));
|
|
||||||
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
|
||||||
} else {
|
|
||||||
// Für einfache Formatierungen wie fett, kursiv, Code
|
|
||||||
formattedText = before + format + selection + format + after;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ersetze den Text
|
|
||||||
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
|
||||||
|
|
||||||
// Setze den Fokus zurück auf das Textarea
|
|
||||||
textarea.focus();
|
|
||||||
|
|
||||||
// Alpine.js Model aktualisieren
|
|
||||||
textarea.dispatchEvent(new Event('input'));
|
|
||||||
|
|
||||||
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
|
||||||
if (selection) {
|
|
||||||
const newStart = start + before.length + format.length;
|
|
||||||
const newEnd = newStart + selection.length;
|
|
||||||
textarea.setSelectionRange(newStart, newEnd);
|
|
||||||
} else {
|
|
||||||
// Setze den Cursor in die Mitte von **|** oder `|`
|
|
||||||
const newCursorPos = start + before.length + format.length;
|
|
||||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Community Forum{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<style>
|
|
||||||
.forum-category {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.forum-category:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
.category-icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
|
||||||
<!-- Seitenüberschrift -->
|
|
||||||
<div class="mb-8 text-center">
|
|
||||||
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
|
|
||||||
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Forumskategorien -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
||||||
{% if categories_data %}
|
|
||||||
{% for cat_data in categories_data %}
|
|
||||||
<a href="{{ url_for('forum_category', category_id=cat_data.category.id) }}" class="forum-category block">
|
|
||||||
<div class="rounded-xl p-5 h-full"
|
|
||||||
x-bind:class="darkMode ? 'bg-gray-800/60 hover:bg-gray-800/80 border border-white/10' : 'bg-white hover:bg-gray-50 border border-gray-200 shadow-md'">
|
|
||||||
<div class="flex items-start">
|
|
||||||
<!-- Kategorie-Icon -->
|
|
||||||
<div class="category-icon mr-4 text-white"
|
|
||||||
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
|
|
||||||
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Kategorie-Info -->
|
|
||||||
<div class="flex-grow">
|
|
||||||
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
|
|
||||||
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
|
|
||||||
|
|
||||||
<!-- Statistik -->
|
|
||||||
<div class="flex flex-wrap gap-4 text-sm opacity-80">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i class="fas fa-comment-alt mr-2"></i>
|
|
||||||
<span>{{ cat_data.total_posts }} Themen</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i class="fas fa-reply mr-2"></i>
|
|
||||||
<span>{{ cat_data.total_replies }} Antworten</span>
|
|
||||||
</div>
|
|
||||||
{% if cat_data.latest_post %}
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i class="fas fa-clock mr-2"></i>
|
|
||||||
<span>Neuster Beitrag: {{ cat_data.latest_post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pfeil-Icon -->
|
|
||||||
<div class="ml-2">
|
|
||||||
<i class="fas fa-chevron-right opacity-50"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<div class="col-span-2 text-center py-8">
|
|
||||||
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
|
|
||||||
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
|
|
||||||
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hinweis zur Nutzung -->
|
|
||||||
<div class="rounded-xl p-6 text-center mb-8"
|
|
||||||
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
|
|
||||||
<h3 class="text-xl font-semibold mb-3">
|
|
||||||
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
|
|
||||||
So funktioniert das Forum
|
|
||||||
</h3>
|
|
||||||
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
|
|
||||||
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
||||||
<div class="p-4 rounded-lg"
|
|
||||||
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
|
||||||
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
|
|
||||||
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
|
|
||||||
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
|
|
||||||
</div>
|
|
||||||
<div class="p-4 rounded-lg"
|
|
||||||
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
|
||||||
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
|
|
||||||
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
|
|
||||||
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
|
|
||||||
</div>
|
|
||||||
<div class="p-4 rounded-lg"
|
|
||||||
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
|
|
||||||
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
|
|
||||||
<h4 class="font-medium mb-1">Markdown Support</h4>
|
|
||||||
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Neues Thema - {{ category.title }}{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<style>
|
|
||||||
.markdown-preview {
|
|
||||||
min-height: 200px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.markdown-preview p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
|
|
||||||
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.markdown-preview h1 { font-size: 1.8rem; }
|
|
||||||
.markdown-preview h2 { font-size: 1.5rem; }
|
|
||||||
.markdown-preview h3 { font-size: 1.3rem; }
|
|
||||||
.markdown-preview h4 { font-size: 1.1rem; }
|
|
||||||
.markdown-preview ul, .markdown-preview ol {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.markdown-preview ul { list-style-type: disc; }
|
|
||||||
.markdown-preview ol { list-style-type: decimal; }
|
|
||||||
.markdown-preview pre {
|
|
||||||
background-color: rgba(0,0,0,0.05);
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
.markdown-preview code {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
padding: 0.1em 0.3em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
background-color: rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
.markdown-preview pre code {
|
|
||||||
padding: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.markdown-preview blockquote {
|
|
||||||
border-left: 4px solid;
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
.dark .markdown-preview code {
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
.dark .markdown-preview blockquote {
|
|
||||||
border-color: rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
.node-mention {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(109, 40, 217, 0.1);
|
|
||||||
color: #6d28d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1px 6px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
margin: 0 2px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.dark .node-mention {
|
|
||||||
background-color: rgba(167, 139, 250, 0.2);
|
|
||||||
color: #a78bfa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
|
||||||
<!-- Breadcrumb Navigation -->
|
|
||||||
<div class="mb-6 flex items-center text-sm">
|
|
||||||
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
|
||||||
<i class="fas fa-home mr-1"></i> Forum
|
|
||||||
</a>
|
|
||||||
<span class="mx-2 opacity-50">/</span>
|
|
||||||
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
|
||||||
{{ category.title }}
|
|
||||||
</a>
|
|
||||||
<span class="mx-2 opacity-50">/</span>
|
|
||||||
<span class="font-medium">Neues Thema</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formular-Header -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<h1 class="text-2xl font-bold mb-2">Neues Thema erstellen</h1>
|
|
||||||
<p class="opacity-75">in der Kategorie <span class="font-medium">{{ category.title }}</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formular -->
|
|
||||||
<div class="mb-8 rounded-xl overflow-hidden"
|
|
||||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
|
||||||
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
|
||||||
<i class="fas fa-plus-circle mr-2"></i>
|
|
||||||
Neues Thema
|
|
||||||
</div>
|
|
||||||
<div class="p-6">
|
|
||||||
<form action="{{ url_for('new_post', category_id=category.id) }}" method="POST" x-data="{
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
showPreview: false,
|
|
||||||
previewHtml: '',
|
|
||||||
|
|
||||||
updatePreview() {
|
|
||||||
// Verarbeite den Inhalt
|
|
||||||
if (this.content.trim() === '') {
|
|
||||||
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verarbeite Markdown
|
|
||||||
let html = marked.parse(this.content);
|
|
||||||
|
|
||||||
// Ersetze @Knotenname mit entsprechenden Links
|
|
||||||
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
|
|
||||||
|
|
||||||
this.previewHtml = html;
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="title" class="block mb-2 font-medium">Titel des Themas</label>
|
|
||||||
<div class="rounded-lg overflow-hidden"
|
|
||||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
|
||||||
<input type="text" id="title" name="title"
|
|
||||||
class="w-full px-4 py-3"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
|
||||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
|
||||||
placeholder="Ein prägnanter Titel für dein Thema"
|
|
||||||
x-model="title"
|
|
||||||
required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<label for="content" class="font-medium">Inhalt</label>
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<button type="button"
|
|
||||||
class="px-3 py-1 rounded text-sm flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
|
||||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
|
||||||
@click="showPreview = false"
|
|
||||||
x-bind:disabled="!showPreview"
|
|
||||||
x-bind:class="{'opacity-50': !showPreview}">
|
|
||||||
<i class="fas fa-edit mr-1"></i> Bearbeiten
|
|
||||||
</button>
|
|
||||||
<button type="button"
|
|
||||||
class="px-3 py-1 rounded text-sm flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
|
||||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
|
|
||||||
@click="updatePreview(); showPreview = true"
|
|
||||||
x-bind:disabled="showPreview"
|
|
||||||
x-bind:class="{'opacity-50': showPreview}">
|
|
||||||
<i class="fas fa-eye mr-1"></i> Vorschau
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Editor -->
|
|
||||||
<div class="rounded-lg overflow-hidden mb-2"
|
|
||||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
|
|
||||||
x-show="!showPreview">
|
|
||||||
<textarea id="content" name="content" rows="12"
|
|
||||||
class="w-full p-3 resize-y"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
|
||||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
|
||||||
placeholder="Schreibe deinen Beitrag hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
|
||||||
x-model="content"
|
|
||||||
required></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preview -->
|
|
||||||
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'border border-white/20 bg-gray-700/30'
|
|
||||||
: 'border border-gray-300 bg-gray-50'"
|
|
||||||
x-show="showPreview"
|
|
||||||
x-html="previewHtml">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Markdown-Hilfsmittel -->
|
|
||||||
<div class="mb-4" x-show="!showPreview">
|
|
||||||
<div class="text-xs opacity-70">
|
|
||||||
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
|
||||||
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
|
||||||
<div class="flex flex-wrap gap-2 mt-1">
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
|
||||||
<i class="fas fa-bold"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
|
||||||
<i class="fas fa-italic"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
|
||||||
<i class="fas fa-code"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
|
||||||
<i class="fas fa-link"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
|
||||||
<i class="fas fa-file-code"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
|
||||||
<i class="fas fa-quote-right"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
|
||||||
<i class="fas fa-list-ul"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
|
||||||
<i class="fas fa-list-ol"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
|
||||||
<i class="fas fa-heading"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<a href="{{ url_for('forum_category', category_id=category.id) }}"
|
|
||||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
|
||||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
|
||||||
Abbrechen
|
|
||||||
</a>
|
|
||||||
<button type="submit"
|
|
||||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
|
||||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
|
||||||
<i class="fas fa-paper-plane mr-2"></i>
|
|
||||||
Thema erstellen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Link zur Mindmap -->
|
|
||||||
<div class="rounded-xl p-5 mb-4 flex items-center"
|
|
||||||
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
|
|
||||||
<div class="text-3xl mr-4 opacity-80">
|
|
||||||
<i class="fas fa-diagram-project" style="color: {{ category.node.color_code }}"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ category.node.name }}</h3>
|
|
||||||
<p class="text-sm opacity-75">Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.</p>
|
|
||||||
</div>
|
|
||||||
<div class="ml-auto">
|
|
||||||
<a href="{{ url_for('mindmap') }}"
|
|
||||||
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
|
|
||||||
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
|
|
||||||
Zur Mindmap
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Markdown-Buttons für den Beitragseditor
|
|
||||||
document.querySelectorAll('.markdown-button').forEach(button => {
|
|
||||||
button.addEventListener('click', function() {
|
|
||||||
const textarea = document.getElementById('content');
|
|
||||||
const format = this.dataset.format;
|
|
||||||
const before = this.dataset.before || '';
|
|
||||||
const after = this.dataset.after || '';
|
|
||||||
|
|
||||||
// Hole die aktuelle Auswahl
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const selection = textarea.value.substring(start, end);
|
|
||||||
|
|
||||||
// Wende die Formatierung an
|
|
||||||
let formattedText;
|
|
||||||
if (format.includes('\n')) {
|
|
||||||
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
|
||||||
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
|
||||||
} else if (format.includes('[Link-Text](URL)')) {
|
|
||||||
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
|
||||||
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
|
||||||
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
|
||||||
const beforeSelection = textarea.value.substring(0, start);
|
|
||||||
const afterSelection = textarea.value.substring(end);
|
|
||||||
|
|
||||||
// Finde den Anfang der aktuellen Zeile
|
|
||||||
const lastNewline = beforeSelection.lastIndexOf('\n');
|
|
||||||
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
|
||||||
|
|
||||||
// Füge das Format am Zeilenanfang ein
|
|
||||||
formattedText = beforeSelection.substring(0, lineStart) +
|
|
||||||
format +
|
|
||||||
beforeSelection.substring(lineStart) +
|
|
||||||
selection +
|
|
||||||
afterSelection;
|
|
||||||
|
|
||||||
// Setze die neue Cursor-Position
|
|
||||||
const newCursorPos = end + format.length;
|
|
||||||
textarea.value = formattedText;
|
|
||||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
||||||
|
|
||||||
// Alpine.js Model aktualisieren
|
|
||||||
textarea.dispatchEvent(new Event('input'));
|
|
||||||
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
|
||||||
} else {
|
|
||||||
// Für einfache Formatierungen wie fett, kursiv, Code
|
|
||||||
formattedText = before + format + selection + format + after;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ersetze den Text
|
|
||||||
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
|
||||||
|
|
||||||
// Setze den Fokus zurück auf das Textarea
|
|
||||||
textarea.focus();
|
|
||||||
|
|
||||||
// Alpine.js Model aktualisieren
|
|
||||||
textarea.dispatchEvent(new Event('input'));
|
|
||||||
|
|
||||||
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
|
||||||
if (selection) {
|
|
||||||
const newStart = start + before.length + format.length;
|
|
||||||
const newEnd = newStart + selection.length;
|
|
||||||
textarea.setSelectionRange(newStart, newEnd);
|
|
||||||
} else {
|
|
||||||
// Setze den Cursor in die Mitte von **|** oder `|`
|
|
||||||
const newCursorPos = start + before.length + format.length;
|
|
||||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,491 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}{{ post.title }} - Forum{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<style>
|
|
||||||
.post-content {
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
.post-content p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.post-content h1, .post-content h2, .post-content h3,
|
|
||||||
.post-content h4, .post-content h5, .post-content h6 {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.post-content h1 { font-size: 1.8rem; }
|
|
||||||
.post-content h2 { font-size: 1.5rem; }
|
|
||||||
.post-content h3 { font-size: 1.3rem; }
|
|
||||||
.post-content h4 { font-size: 1.1rem; }
|
|
||||||
.post-content ul, .post-content ol {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.post-content ul { list-style-type: disc; }
|
|
||||||
.post-content ol { list-style-type: decimal; }
|
|
||||||
.post-content pre {
|
|
||||||
background-color: rgba(0,0,0,0.05);
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
.post-content code {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
padding: 0.1em 0.3em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
background-color: rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
.post-content pre code {
|
|
||||||
padding: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.post-content blockquote {
|
|
||||||
border-left: 4px solid;
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
.post-content img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
.post-content table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
.post-content th, .post-content td {
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid;
|
|
||||||
border-color: rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.post-content th {
|
|
||||||
background-color: rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
.post-content a {
|
|
||||||
color: #6d28d9;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.post-content a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.dark .post-content code {
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
.dark .post-content th, .dark .post-content td {
|
|
||||||
border-color: rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
.dark .post-content th {
|
|
||||||
background-color: rgba(255,255,255,0.05);
|
|
||||||
}
|
|
||||||
.dark .post-content a {
|
|
||||||
color: #a78bfa;
|
|
||||||
}
|
|
||||||
.node-mention {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(109, 40, 217, 0.1);
|
|
||||||
color: #6d28d9;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1px 6px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
margin: 0 2px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.dark .node-mention {
|
|
||||||
background-color: rgba(167, 139, 250, 0.2);
|
|
||||||
color: #a78bfa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
|
||||||
<!-- Breadcrumb Navigation -->
|
|
||||||
<div class="mb-6 flex items-center text-sm">
|
|
||||||
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
|
||||||
<i class="fas fa-home mr-1"></i> Forum
|
|
||||||
</a>
|
|
||||||
<span class="mx-2 opacity-50">/</span>
|
|
||||||
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
|
|
||||||
{{ category.title }}
|
|
||||||
</a>
|
|
||||||
<span class="mx-2 opacity-50">/</span>
|
|
||||||
<span class="font-medium truncate max-w-[300px]">{{ post.title }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Beitrags-Header -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<h1 class="text-2xl font-bold mb-2">{{ post.title }}</h1>
|
|
||||||
<div class="flex flex-wrap items-center gap-3 text-sm opacity-75">
|
|
||||||
<span><i class="fas fa-calendar-alt mr-1"></i> {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
|
|
||||||
<span><i class="fas fa-eye mr-1"></i> {{ post.view_count }} Aufrufe</span>
|
|
||||||
<span><i class="fas fa-reply mr-1"></i> {{ replies|length }} Antworten</span>
|
|
||||||
|
|
||||||
{% if post.is_pinned or post.is_locked %}
|
|
||||||
<div class="flex gap-2 ml-2">
|
|
||||||
{% if post.is_pinned %}
|
|
||||||
<span class="px-2 py-0.5 text-xs rounded-full"
|
|
||||||
x-bind:class="darkMode ? 'bg-yellow-700/50 text-yellow-300' : 'bg-yellow-100 text-yellow-800'">
|
|
||||||
<i class="fas fa-thumbtack mr-1"></i> Angepinnt
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if post.is_locked %}
|
|
||||||
<span class="px-2 py-0.5 text-xs rounded-full"
|
|
||||||
x-bind:class="darkMode ? 'bg-red-700/50 text-red-300' : 'bg-red-100 text-red-800'">
|
|
||||||
<i class="fas fa-lock mr-1"></i> Gesperrt
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hauptbeitrag -->
|
|
||||||
<div class="mb-8 rounded-xl overflow-hidden"
|
|
||||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-sm'">
|
|
||||||
<!-- Beitrags-Header -->
|
|
||||||
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<!-- Autor-Avatar -->
|
|
||||||
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden mr-3"
|
|
||||||
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
|
||||||
{% if post.author.avatar %}
|
|
||||||
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
|
|
||||||
{% else %}
|
|
||||||
{{ post.author.username[0].upper() }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Autor-Info -->
|
|
||||||
<div>
|
|
||||||
<div class="font-medium">{{ post.author.username }}</div>
|
|
||||||
<div class="text-xs opacity-70">Erstellt am {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Aktionen -->
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
{% if current_user.id == post.user_id or current_user.role == 'admin' %}
|
|
||||||
<a href="{{ url_for('edit_post', post_id=post.id) }}"
|
|
||||||
class="p-2 rounded transition-colors"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'hover:bg-gray-700/50 text-gray-300'
|
|
||||||
: 'hover:bg-gray-100 text-gray-600'">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
<form action="{{ url_for('delete_post', post_id=post.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diesen Beitrag wirklich löschen?');">
|
|
||||||
<button type="submit"
|
|
||||||
class="p-2 rounded transition-colors"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'hover:bg-red-800/50 text-red-300'
|
|
||||||
: 'hover:bg-red-100 text-red-600'">
|
|
||||||
<i class="fas fa-trash-alt"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Moderation-Optionen -->
|
|
||||||
{% if current_user.role in ['admin', 'moderator'] %}
|
|
||||||
<div class="ml-2 border-l" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'"></div>
|
|
||||||
<form action="{{ url_for('toggle_pin_post', post_id=post.id) }}" method="POST" class="inline">
|
|
||||||
<button type="submit"
|
|
||||||
class="p-2 rounded transition-colors"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'hover:bg-yellow-800/50 text-yellow-300'
|
|
||||||
: 'hover:bg-yellow-100 text-yellow-600'"
|
|
||||||
title="{% if post.is_pinned %}Nicht mehr anpinnen{% else %}Anpinnen{% endif %}">
|
|
||||||
<i class="fas fa-thumbtack"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form action="{{ url_for('toggle_lock_post', post_id=post.id) }}" method="POST" class="inline">
|
|
||||||
<button type="submit"
|
|
||||||
class="p-2 rounded transition-colors"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'hover:bg-blue-800/50 text-blue-300'
|
|
||||||
: 'hover:bg-blue-100 text-blue-600'"
|
|
||||||
title="{% if post.is_locked %}Entsperren{% else %}Sperren{% endif %}">
|
|
||||||
<i class="fas {% if post.is_locked %}fa-unlock{% else %}fa-lock{% endif %}"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Beitrags-Inhalt -->
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="post-content markdown-content" id="main-post-content">
|
|
||||||
{{ post.content|safe }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if post.updated_at and post.updated_at != post.created_at %}
|
|
||||||
<div class="mt-6 pt-4 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
|
||||||
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ post.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Antworten-Bereich -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4">
|
|
||||||
<i class="fas fa-reply mr-2 opacity-60"></i>
|
|
||||||
{{ replies|length }} Antworten
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<!-- Antworten-Liste -->
|
|
||||||
{% if replies %}
|
|
||||||
{% for reply in replies %}
|
|
||||||
<div class="mb-5 rounded-xl overflow-hidden"
|
|
||||||
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
|
||||||
<!-- Antwort-Header -->
|
|
||||||
<div class="p-3 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<!-- Autor-Avatar -->
|
|
||||||
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white font-medium text-xs overflow-hidden mr-3"
|
|
||||||
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
|
||||||
{% if reply.author.avatar %}
|
|
||||||
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
|
|
||||||
{% else %}
|
|
||||||
{{ reply.author.username[0].upper() }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Autor-Info -->
|
|
||||||
<div>
|
|
||||||
<div class="font-medium text-sm">{{ reply.author.username }}</div>
|
|
||||||
<div class="text-xs opacity-70">{{ reply.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Aktionen -->
|
|
||||||
<div class="flex items-center space-x-1">
|
|
||||||
{% if current_user.id == reply.user_id or current_user.role == 'admin' %}
|
|
||||||
<a href="{{ url_for('edit_post', post_id=reply.id) }}"
|
|
||||||
class="p-1.5 rounded text-sm transition-colors"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'hover:bg-gray-700/50 text-gray-300'
|
|
||||||
: 'hover:bg-gray-100 text-gray-600'">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
<form action="{{ url_for('delete_post', post_id=reply.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diese Antwort wirklich löschen?');">
|
|
||||||
<button type="submit"
|
|
||||||
class="p-1.5 rounded text-sm transition-colors"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'hover:bg-red-800/50 text-red-300'
|
|
||||||
: 'hover:bg-red-100 text-red-600'">
|
|
||||||
<i class="fas fa-trash-alt"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Antwort-Inhalt -->
|
|
||||||
<div class="p-5">
|
|
||||||
<div class="post-content markdown-content reply-content" id="reply-content-{{ reply.id }}">
|
|
||||||
{{ reply.content|safe }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if reply.updated_at and reply.updated_at != reply.created_at %}
|
|
||||||
<div class="mt-4 pt-3 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
|
||||||
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ reply.updated_at.strftime('%d.%m.%Y, %H:%M') }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<div class="rounded-xl p-6 text-center"
|
|
||||||
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
|
|
||||||
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
|
|
||||||
<h3 class="text-lg font-semibold mb-2">Noch keine Antworten</h3>
|
|
||||||
<p class="opacity-75">Sei der Erste, der auf diesen Beitrag antwortet!</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Antwort-Formular -->
|
|
||||||
{% if not post.is_locked %}
|
|
||||||
<div class="mb-8 rounded-xl overflow-hidden"
|
|
||||||
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
|
|
||||||
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
|
|
||||||
<i class="fas fa-reply mr-2"></i>
|
|
||||||
Antworten
|
|
||||||
</div>
|
|
||||||
<div class="p-6">
|
|
||||||
<form action="{{ url_for('reply_to_post', post_id=post.id) }}" method="POST">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="content" class="block mb-2 font-medium">Deine Antwort</label>
|
|
||||||
<div class="mb-2 rounded-lg overflow-hidden"
|
|
||||||
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
|
|
||||||
<textarea id="content" name="content" rows="6"
|
|
||||||
class="w-full p-3 resize-y"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
|
|
||||||
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
|
|
||||||
placeholder="Schreibe deine Antwort hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
|
|
||||||
required></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs opacity-70">
|
|
||||||
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
|
|
||||||
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
|
|
||||||
<div class="flex flex-wrap gap-2 mt-1">
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
|
|
||||||
<i class="fas fa-bold"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
|
|
||||||
<i class="fas fa-italic"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
|
|
||||||
<i class="fas fa-code"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
|
|
||||||
<i class="fas fa-link"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
|
|
||||||
<i class="fas fa-file-code"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
|
|
||||||
<i class="fas fa-quote-right"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
|
|
||||||
<i class="fas fa-list-ul"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
|
|
||||||
<i class="fas fa-list-ol"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
|
|
||||||
<i class="fas fa-heading"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button type="submit"
|
|
||||||
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
|
|
||||||
x-bind:class="darkMode
|
|
||||||
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
|
|
||||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
|
|
||||||
<i class="fas fa-paper-plane mr-2"></i>
|
|
||||||
Antwort senden
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="rounded-xl p-5 text-center mb-6"
|
|
||||||
x-bind:class="darkMode ? 'bg-red-900/20 border border-red-800/30' : 'bg-red-50 border border-red-100'">
|
|
||||||
<i class="fas fa-lock mr-2 text-red-500"></i>
|
|
||||||
<span>Dieser Beitrag ist geschlossen. Es können keine neuen Antworten mehr verfasst werden.</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Markdown und Knotenerwähnungen verarbeiten
|
|
||||||
const processContent = (content) => {
|
|
||||||
// Verarbeite Markdown mit marked.js
|
|
||||||
let html = marked.parse(content);
|
|
||||||
|
|
||||||
// Ersetze @Knotenname mit entsprechenden Links
|
|
||||||
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class="node-mention"><i class="fas fa-diagram-project fa-xs mr-1"></i>$1</span>');
|
|
||||||
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Markdown-Inhalt für Hauptbeitrag rendern
|
|
||||||
const mainPostContent = document.getElementById('main-post-content');
|
|
||||||
if (mainPostContent) {
|
|
||||||
mainPostContent.innerHTML = processContent(mainPostContent.textContent.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Markdown-Inhalt für Antworten rendern
|
|
||||||
document.querySelectorAll('.reply-content').forEach(reply => {
|
|
||||||
reply.innerHTML = processContent(reply.textContent.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Markdown-Buttons für das Antwortformular
|
|
||||||
document.querySelectorAll('.markdown-button').forEach(button => {
|
|
||||||
button.addEventListener('click', function() {
|
|
||||||
const textarea = document.getElementById('content');
|
|
||||||
const format = this.dataset.format;
|
|
||||||
const before = this.dataset.before || '';
|
|
||||||
const after = this.dataset.after || '';
|
|
||||||
|
|
||||||
// Hole die aktuelle Auswahl
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const selection = textarea.value.substring(start, end);
|
|
||||||
|
|
||||||
// Wende die Formatierung an
|
|
||||||
let formattedText;
|
|
||||||
if (format.includes('\n')) {
|
|
||||||
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
|
|
||||||
formattedText = format.replace('Code-Block', selection || 'Code-Block');
|
|
||||||
} else if (format.includes('[Link-Text](URL)')) {
|
|
||||||
formattedText = format.replace('Link-Text', selection || 'Link-Text');
|
|
||||||
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
|
|
||||||
// Für Listen und Überschriften: am Anfang der Zeile einfügen
|
|
||||||
const beforeSelection = textarea.value.substring(0, start);
|
|
||||||
const afterSelection = textarea.value.substring(end);
|
|
||||||
|
|
||||||
// Finde den Anfang der aktuellen Zeile
|
|
||||||
const lastNewline = beforeSelection.lastIndexOf('\n');
|
|
||||||
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
|
|
||||||
|
|
||||||
// Füge das Format am Zeilenanfang ein
|
|
||||||
formattedText = beforeSelection.substring(0, lineStart) +
|
|
||||||
format +
|
|
||||||
beforeSelection.substring(lineStart) +
|
|
||||||
selection +
|
|
||||||
afterSelection;
|
|
||||||
|
|
||||||
// Setze die neue Cursor-Position
|
|
||||||
const newCursorPos = end + format.length;
|
|
||||||
textarea.value = formattedText;
|
|
||||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
||||||
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
|
|
||||||
} else {
|
|
||||||
// Für einfache Formatierungen wie fett, kursiv, Code
|
|
||||||
formattedText = before + format + selection + format + after;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ersetze den Text
|
|
||||||
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
|
|
||||||
|
|
||||||
// Setze den Fokus zurück auf das Textarea
|
|
||||||
textarea.focus();
|
|
||||||
|
|
||||||
// Setze die Auswahl neu, wenn es eine Auswahl gab
|
|
||||||
if (selection) {
|
|
||||||
const newStart = start + before.length + format.length;
|
|
||||||
const newEnd = newStart + selection.length;
|
|
||||||
textarea.setSelectionRange(newStart, newEnd);
|
|
||||||
} else {
|
|
||||||
// Setze den Cursor in die Mitte von **|** oder `|`
|
|
||||||
const newCursorPos = start + before.length + format.length;
|
|
||||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,265 +1,234 @@
|
|||||||
{% extends "base.html" %}
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Interaktive Mindmap</title>
|
||||||
|
|
||||||
{% block title %}Mindmap{% endblock %}
|
<!-- Cytoscape.js -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||||
|
|
||||||
{% block extra_css %}
|
<!-- Socket.IO -->
|
||||||
<style>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mindmap-container {
|
<!-- Feather Icons (optional) -->
|
||||||
position: relative;
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .mindmap-container {
|
<style>
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
* {
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
.mindmap-toolbar {
|
padding: 0;
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background-color: var(--bg-secondary);
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.dark) .mindmap-toolbar {
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background-color: var(--bg-secondary);
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-filter {
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-filter:not(.active) {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-filter:hover:not(.active) {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Kontextmenü */
|
|
||||||
#context-menu {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
z-index: 1000;
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark #context-menu {
|
|
||||||
background-color: #232837;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.dark) #context-menu {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#context-menu .menu-item {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark #context-menu .menu-item:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.dark) #context-menu .menu-item:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Zusätzliches Layout */
|
|
||||||
.mindmap-section {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.mindmap-section {
|
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
body {
|
||||||
<div class="container mx-auto px-4 py-8">
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
<div class="flex flex-col lg:flex-row gap-8">
|
background-color: #f9fafb;
|
||||||
<!-- Hauptinhalt -->
|
color: #111827;
|
||||||
<div class="w-full lg:w-3/4">
|
line-height: 1.5;
|
||||||
<!-- Mindmap-Titelbereich -->
|
}
|
||||||
<div class="mb-6">
|
|
||||||
<h1 class="text-3xl font-bold mb-2 mystical-glow"
|
.container {
|
||||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
display: flex;
|
||||||
Wissenslandkarte
|
flex-direction: column;
|
||||||
</h1>
|
height: 100vh;
|
||||||
<p class="opacity-80 text-lg"
|
width: 100%;
|
||||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
}
|
||||||
Visualisiere die Verbindungen zwischen Gedanken und Konzepten
|
|
||||||
</p>
|
.header {
|
||||||
|
background-color: #1f2937;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
padding: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cy {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-filter {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-filter:not(.active) {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-filter:hover:not(.active) {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kontextmenü Styling */
|
||||||
|
#context-menu {
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#context-menu .menu-item {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#context-menu .menu-item:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>Interaktive Mindmap</h1>
|
||||||
|
<div class="search-container">
|
||||||
|
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Mindmap-Container -->
|
<div class="toolbar">
|
||||||
<div class="mindmap-container">
|
<button id="addNode" class="btn">
|
||||||
<!-- Toolbar -->
|
<i data-feather="plus-circle"></i>
|
||||||
<div class="mindmap-toolbar">
|
Knoten hinzufügen
|
||||||
<button id="fit-btn" class="mindmap-action-btn">
|
</button>
|
||||||
<i class="fa-solid fa-expand"></i>
|
<button id="addEdge" class="btn">
|
||||||
<span>Ansicht anpassen</span>
|
<i data-feather="git-branch"></i>
|
||||||
</button>
|
Verbindung erstellen
|
||||||
<button id="reset-btn" class="mindmap-action-btn">
|
</button>
|
||||||
<i class="fa-solid fa-undo"></i>
|
<button id="editNode" class="btn btn-secondary">
|
||||||
<span>Zurücksetzen</span>
|
<i data-feather="edit-2"></i>
|
||||||
</button>
|
Knoten bearbeiten
|
||||||
<button id="toggle-labels-btn" class="mindmap-action-btn">
|
</button>
|
||||||
<i class="fa-solid fa-tags"></i>
|
<button id="deleteNode" class="btn btn-danger">
|
||||||
<span>Labels ein/aus</span>
|
<i data-feather="trash-2"></i>
|
||||||
</button>
|
Knoten löschen
|
||||||
</div>
|
</button>
|
||||||
|
<button id="deleteEdge" class="btn btn-danger">
|
||||||
<!-- Hauptvisualisierung -->
|
<i data-feather="scissors"></i>
|
||||||
<div id="cy"></div>
|
Verbindung löschen
|
||||||
|
</button>
|
||||||
<!-- Info-Panel -->
|
<button id="reLayout" class="btn btn-secondary">
|
||||||
<div id="node-info-panel" class="mindmap-info-panel">
|
<i data-feather="refresh-cw"></i>
|
||||||
<h4 class="info-panel-title">Knoteninfo</h4>
|
Layout neu anordnen
|
||||||
<p id="node-description" class="info-panel-description">Wählen Sie einen Knoten aus...</p>
|
</button>
|
||||||
|
<button id="exportMindmap" class="btn btn-secondary">
|
||||||
<div class="node-navigation">
|
<i data-feather="download"></i>
|
||||||
<h5 class="node-navigation-title">Verknüpfte Knoten</h5>
|
Exportieren
|
||||||
<div id="connected-nodes" class="node-links">
|
</button>
|
||||||
<!-- Wird dynamisch befüllt -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Seitenleiste -->
|
<div id="category-filters" class="category-filters">
|
||||||
<div class="w-full lg:w-1/4 space-y-6">
|
<!-- Wird dynamisch befüllt -->
|
||||||
<!-- 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>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_js %}
|
<!-- Unsere Mindmap JS -->
|
||||||
<script>
|
<script src="{{ url_for('static', filename='js/mindmap.js') }}"></script>
|
||||||
// Sobald die Seite und die Scripte geladen sind, initialisiere die Mindmap
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
<!-- Icons initialisieren -->
|
||||||
if (window.MindMap && window.MindMap.pageInitializers && window.MindMap.pageInitializers.mindmap) {
|
<script>
|
||||||
window.MindMap.pageInitializers.mindmap();
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
} else {
|
if (typeof feather !== 'undefined') {
|
||||||
console.error('Mindmap-Initialisierung konnte nicht gefunden werden!');
|
feather.replace();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe
|
executable = C:\Program Files\Python313\python.exe
|
||||||
command = C:\Users\firem\Desktop\111\Systades\website\.venv\Scripts\python.exe -m venv C:\Users\firem\Desktop\111\Systades\website\venv
|
command = C:\Program Files\Python313\python.exe -m venv C:\Users\TTOMCZA.EMEA\Dev\website\venv
|
||||||
|
|||||||
58
windows_setup.bat
Normal file
58
windows_setup.bat
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
@echo off
|
||||||
|
echo Mindmap Projekt - Windows Setup
|
||||||
|
echo ==============================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Prüfen, ob Python installiert ist
|
||||||
|
python --version >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo Python ist nicht installiert oder nicht im PATH.
|
||||||
|
echo Bitte installiere Python 3.11 von https://www.python.org/downloads/
|
||||||
|
echo und stelle sicher, dass "Add Python to PATH" während der Installation aktiviert ist.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Erstelle virtuelle Umgebung...
|
||||||
|
python -m venv venv
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo Fehler beim Erstellen der virtuellen Umgebung.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Aktiviere virtuelle Umgebung...
|
||||||
|
call venv\Scripts\activate.bat
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo Fehler beim Aktivieren der virtuellen Umgebung.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Aktualisiere pip...
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo Warnung: Pip konnte nicht aktualisiert werden. Fahre trotzdem fort.
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Installiere Abhängigkeiten...
|
||||||
|
pip install -r requirements.txt
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo Fehler beim Installieren der Abhängigkeiten.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Setup abgeschlossen!
|
||||||
|
echo.
|
||||||
|
echo Zum Starten des Servers:
|
||||||
|
echo 1. Führe "venv\Scripts\activate.bat" aus
|
||||||
|
echo 2. Führe "python TOOLS.py db:rebuild" aus (Nur beim ersten Mal oder zum Zurücksetzen der Datenbank)
|
||||||
|
echo 3. Führe "python TOOLS.py user:admin" aus (Erstellt einen Admin-Benutzer: admin/admin)
|
||||||
|
echo 4. Führe "python TOOLS.py server:run" aus
|
||||||
|
echo.
|
||||||
|
echo Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
||||||
|
echo.
|
||||||
|
|
||||||
|
pause
|
||||||
Reference in New Issue
Block a user