491 lines
21 KiB
HTML
491 lines
21 KiB
HTML
{% 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 %} |