|
{% extends "layout.html" %} |
|
|
|
{% block title %}{{ topic.title }} - Community Forum{% endblock %} |
|
|
|
{% block breadcrumb %} |
|
<a href="{{ url_for('forum.index') }}" class="hover:text-blue-600">Home</a> |
|
<span class="mx-2">/</span> |
|
<a href="{{ url_for('forum.category_list') }}" class="hover:text-blue-600">Categories</a> |
|
<span class="mx-2">/</span> |
|
<a href="{{ url_for('forum.category_view', id=topic.category_id) }}" class="hover:text-blue-600">{{ topic.category.name }}</a> |
|
<span class="mx-2">/</span> |
|
<span>{{ topic.title }}</span> |
|
{% endblock %} |
|
|
|
{% block content %} |
|
<div class="bg-white rounded-lg shadow overflow-hidden"> |
|
<div class="px-6 py-4 border-b border-gray-200 flex flex-wrap justify-between items-center gap-2"> |
|
<div> |
|
<h1 class="text-2xl font-bold text-gray-800">{{ topic.title }}</h1> |
|
|
|
<div class="mt-1 flex flex-wrap items-center gap-2"> |
|
{% if topic.tags %} |
|
{% for tag in topic.tags %} |
|
<a href="{{ url_for('forum.tag_view', tag_name=tag.name) }}" class="tag"> |
|
{{ tag.name }} |
|
</a> |
|
{% endfor %} |
|
{% endif %} |
|
|
|
{% if topic.is_pinned %} |
|
<span class="flex items-center text-xs text-yellow-600 bg-yellow-100 px-2 py-1 rounded-full"> |
|
<i data-feather="star" class="w-3 h-3 mr-1"></i> Pinned |
|
</span> |
|
{% endif %} |
|
|
|
{% if topic.is_locked %} |
|
<span class="flex items-center text-xs text-red-600 bg-red-100 px-2 py-1 rounded-full"> |
|
<i data-feather="lock" class="w-3 h-3 mr-1"></i> Locked |
|
</span> |
|
{% endif %} |
|
</div> |
|
</div> |
|
|
|
<div class="flex flex-wrap gap-2"> |
|
{% if current_user.is_authenticated and current_user.is_moderator() %} |
|
<form method="POST" action="{{ url_for('forum.pin_topic', id=topic.id) }}" class="inline"> |
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> |
|
<button type="submit" id="pin-topic-btn" data-is-pinned="{{ 'true' if topic.is_pinned else 'false' }}" class="px-3 py-1 bg-yellow-100 text-yellow-700 rounded hover:bg-yellow-200 focus:outline-none focus:ring"> |
|
{% if topic.is_pinned %} |
|
<i data-feather="star-off" class="w-4 h-4 inline-block mr-1"></i> Unpin |
|
{% else %} |
|
<i data-feather="star" class="w-4 h-4 inline-block mr-1"></i> Pin |
|
{% endif %} |
|
</button> |
|
</form> |
|
|
|
<form method="POST" action="{{ url_for('forum.lock_topic', id=topic.id) }}" class="inline"> |
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> |
|
<button type="submit" id="lock-topic-btn" data-is-locked="{{ 'true' if topic.is_locked else 'false' }}" class="px-3 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 focus:outline-none focus:ring"> |
|
{% if topic.is_locked %} |
|
<i data-feather="unlock" class="w-4 h-4 inline-block mr-1"></i> Unlock |
|
{% else %} |
|
<i data-feather="lock" class="w-4 h-4 inline-block mr-1"></i> Lock |
|
{% endif %} |
|
</button> |
|
</form> |
|
{% endif %} |
|
|
|
{% if current_user.is_authenticated %} |
|
<button class="report-button px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200 focus:outline-none focus:ring" data-topic-id="{{ topic.id }}"> |
|
<i data-feather="flag" class="w-4 h-4 inline-block mr-1"></i> Report |
|
</button> |
|
{% endif %} |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="divide-y divide-gray-200"> |
|
{% for post in posts.items %} |
|
<div id="post-{{ post.id }}" class="post flex flex-col md:flex-row p-0 md:divide-x divide-gray-200"> |
|
|
|
<div class="w-full md:w-56 lg:w-64 p-4 md:p-6 bg-gray-50"> |
|
<div class="flex flex-row md:flex-col items-center md:items-start"> |
|
<div class="flex-shrink-0 mr-4 md:mr-0 md:mb-3"> |
|
<img |
|
src="{{ url_for('static', filename='uploads/avatars/' + post.author.avatar) if post.author.avatar else url_for('static', filename='uploads/avatars/default.png') }}" |
|
alt="{{ post.author.username }}" |
|
class="w-12 h-12 md:w-16 md:h-16 rounded-full object-cover" |
|
> |
|
</div> |
|
<div> |
|
<div class="post-author"> |
|
<a href="{{ url_for('user.profile', username=post.author.username) }}" class="text-blue-600 font-medium hover:underline"> |
|
{{ post.author.username }} |
|
</a> |
|
</div> |
|
<div class="text-gray-500 text-sm"> |
|
{% if post.author.role == 'admin' %} |
|
<span class="inline-block bg-red-100 text-red-800 text-xs px-2 py-1 rounded-full">Admin</span> |
|
{% elif post.author.role == 'moderator' %} |
|
<span class="inline-block bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full">Moderator</span> |
|
{% else %} |
|
<span class="inline-block bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full">Member</span> |
|
{% endif %} |
|
</div> |
|
<div class="text-gray-500 text-xs mt-2 hidden md:block"> |
|
Joined: {{ post.author.created_at.strftime('%b %Y') }} |
|
</div> |
|
</div> |
|
</div> |
|
|
|
{% if post.author.signature and loop.index > 1 %} |
|
<div class="mt-4 pt-4 border-t border-gray-200 text-xs text-gray-500 hidden md:block"> |
|
{{ post.author.signature }} |
|
</div> |
|
{% endif %} |
|
</div> |
|
|
|
|
|
<div class="flex-1 p-4 md:p-6"> |
|
<div class="flex justify-between items-start mb-4"> |
|
<div class="text-sm text-gray-500"> |
|
<a href="#post-{{ post.id }}" class="hover:text-blue-600"> |
|
{{ post.created_at.strftime('%b %d, %Y %H:%M') }} |
|
</a> |
|
{% if post.updated_at and post.updated_at != post.created_at %} |
|
<span class="text-xs ml-2"> |
|
(Edited {% if post.edited_by %}by {{ post.edited_by.username }}{% endif %} |
|
on {{ post.updated_at.strftime('%b %d, %Y %H:%M') }}) |
|
</span> |
|
{% endif %} |
|
</div> |
|
<div class="flex space-x-1"> |
|
<a href="{{ url_for('forum.quote_post', id=post.id) }}" class="quote-button p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded"> |
|
<i data-feather="message-square" class="w-4 h-4"></i> |
|
<span class="sr-only">Quote</span> |
|
</a> |
|
|
|
{% if current_user.is_authenticated and (current_user.id == post.author_id or current_user.is_moderator()) %} |
|
<a href="{{ url_for('forum.edit_post', id=post.id) }}" class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded"> |
|
<i data-feather="edit" class="w-4 h-4"></i> |
|
<span class="sr-only">Edit</span> |
|
</a> |
|
|
|
<form method="POST" action="{{ url_for('forum.delete_post', id=post.id) }}" class="inline"> |
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> |
|
<button type="submit" class="delete-button p-1 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded"> |
|
<i data-feather="trash-2" class="w-4 h-4"></i> |
|
<span class="sr-only">Delete</span> |
|
</button> |
|
</form> |
|
{% endif %} |
|
|
|
{% if current_user.is_authenticated and current_user.id != post.author_id %} |
|
<button class="report-button p-1 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded" data-post-id="{{ post.id }}"> |
|
<i data-feather="flag" class="w-4 h-4"></i> |
|
<span class="sr-only">Report</span> |
|
</button> |
|
{% endif %} |
|
</div> |
|
</div> |
|
|
|
<div class="post-content prose max-w-none"> |
|
{{ post.content|safe }} |
|
</div> |
|
|
|
|
|
{% if current_user.is_authenticated %} |
|
<div class="mt-6 flex space-x-2"> |
|
<button class="reaction-btn {% if current_user.is_authenticated and post.reactions.filter_by(user_id=current_user.id, reaction_type='like').first() %}active{% endif %}" data-post-id="{{ post.id }}" data-reaction-type="like"> |
|
<i data-feather="thumbs-up" class="w-4 h-4"></i> |
|
<span class="reaction-count {% if post.get_reaction_count('like') == 0 %}hidden{% endif %}">{{ post.get_reaction_count('like') }}</span> |
|
</button> |
|
|
|
<button class="reaction-btn {% if current_user.is_authenticated and post.reactions.filter_by(user_id=current_user.id, reaction_type='heart').first() %}active{% endif %}" data-post-id="{{ post.id }}" data-reaction-type="heart"> |
|
<i data-feather="heart" class="w-4 h-4"></i> |
|
<span class="reaction-count {% if post.get_reaction_count('heart') == 0 %}hidden{% endif %}">{{ post.get_reaction_count('heart') }}</span> |
|
</button> |
|
|
|
<button class="reaction-btn {% if current_user.is_authenticated and post.reactions.filter_by(user_id=current_user.id, reaction_type='smile').first() %}active{% endif %}" data-post-id="{{ post.id }}" data-reaction-type="smile"> |
|
<i data-feather="smile" class="w-4 h-4"></i> |
|
<span class="reaction-count {% if post.get_reaction_count('smile') == 0 %}hidden{% endif %}">{{ post.get_reaction_count('smile') }}</span> |
|
</button> |
|
</div> |
|
{% endif %} |
|
</div> |
|
</div> |
|
{% endfor %} |
|
</div> |
|
|
|
|
|
{% if posts.pages > 1 %} |
|
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200"> |
|
<div class="flex justify-center"> |
|
<nav class="inline-flex rounded-md shadow"> |
|
{% if posts.has_prev %} |
|
<a href="{{ url_for('forum.topic_view', id=topic.id, page=posts.prev_num) }}" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50"> |
|
Previous |
|
</a> |
|
{% else %} |
|
<span class="px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 rounded-l-md cursor-not-allowed"> |
|
Previous |
|
</span> |
|
{% endif %} |
|
|
|
{% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %} |
|
{% if page_num %} |
|
{% if page_num == posts.page %} |
|
<span class="px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 border border-gray-300"> |
|
{{ page_num }} |
|
</span> |
|
{% else %} |
|
<a href="{{ url_for('forum.topic_view', id=topic.id, page=page_num) }}" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50"> |
|
{{ page_num }} |
|
</a> |
|
{% endif %} |
|
{% else %} |
|
<span class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300"> |
|
… |
|
</span> |
|
{% endif %} |
|
{% endfor %} |
|
|
|
{% if posts.has_next %} |
|
<a href="{{ url_for('forum.topic_view', id=topic.id, page=posts.next_num) }}" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50"> |
|
Next |
|
</a> |
|
{% else %} |
|
<span class="px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 rounded-r-md cursor-not-allowed"> |
|
Next |
|
</span> |
|
{% endif %} |
|
</nav> |
|
</div> |
|
</div> |
|
{% endif %} |
|
|
|
|
|
{% if current_user.is_authenticated and not topic.is_locked or (current_user.is_authenticated and current_user.is_moderator()) %} |
|
<div id="reply-form" class="p-6 bg-gray-50 border-t border-gray-200"> |
|
<h3 class="text-lg font-medium text-gray-800 mb-4">Post a Reply</h3> |
|
|
|
<form method="POST" action="{{ url_for('forum.topic_view', id=topic.id) }}"> |
|
{{ post_form.hidden_tag() }} |
|
{{ post_form.topic_id(value=topic.id) }} |
|
|
|
<div class="editor-container mb-4"> |
|
<div class="editor-toolbar bg-white border border-gray-300"> |
|
|
|
</div> |
|
|
|
{{ post_form.content(class="w-full px-4 py-2 border border-gray-300 editor-textarea focus:outline-none focus:border-blue-500 focus:ring focus:ring-blue-200", id="content", rows="6") }} |
|
|
|
{% if post_form.content.errors %} |
|
<div class="text-red-600 text-sm mt-1"> |
|
{% for error in post_form.content.errors %} |
|
<p>{{ error }}</p> |
|
{% endfor %} |
|
</div> |
|
{% endif %} |
|
</div> |
|
|
|
<div class="mt-4"> |
|
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"> |
|
Post Reply |
|
</button> |
|
</div> |
|
</form> |
|
</div> |
|
{% elif current_user.is_authenticated and topic.is_locked %} |
|
<div class="p-6 bg-gray-50 border-t border-gray-200"> |
|
<div class="flex items-center justify-center p-4 bg-red-50 text-red-700 rounded-md"> |
|
<i data-feather="lock" class="w-5 h-5 mr-2"></i> |
|
<span>This topic is locked. New replies are not allowed.</span> |
|
</div> |
|
</div> |
|
{% elif not current_user.is_authenticated %} |
|
<div class="p-6 bg-gray-50 border-t border-gray-200"> |
|
<div class="flex items-center justify-center p-4 bg-blue-50 text-blue-700 rounded-md"> |
|
<i data-feather="info" class="w-5 h-5 mr-2"></i> |
|
<span>Please <a href="{{ url_for('auth.login') }}" class="underline font-medium">log in</a> to post a reply.</span> |
|
</div> |
|
</div> |
|
{% endif %} |
|
</div> |
|
|
|
|
|
{% if current_user.is_authenticated %} |
|
<div id="report-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"> |
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-md"> |
|
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center"> |
|
<h3 class="text-lg font-medium text-gray-800">Report Content</h3> |
|
<button type="button" class="close-modal text-gray-400 hover:text-gray-500"> |
|
<i data-feather="x" class="w-5 h-5"></i> |
|
<span class="sr-only">Close</span> |
|
</button> |
|
</div> |
|
|
|
<form id="report-form" method="POST" action="{{ url_for('forum.create_report') }}"> |
|
{{ report_form.hidden_tag() }} |
|
{{ report_form.post_id(id="post_id") }} |
|
{{ report_form.topic_id(id="topic_id") }} |
|
|
|
<div class="p-6"> |
|
<div class="mb-4"> |
|
<label for="reason" class="block text-gray-700 font-medium mb-2">Reason for Report</label> |
|
{{ report_form.reason(class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-blue-500 focus:ring focus:ring-blue-200", id="reason", rows="4", placeholder="Please provide a detailed explanation of why you're reporting this content...") }} |
|
{% if report_form.reason.errors %} |
|
<div class="text-red-600 text-sm mt-1"> |
|
{% for error in report_form.reason.errors %} |
|
<p>{{ error }}</p> |
|
{% endfor %} |
|
</div> |
|
{% endif %} |
|
</div> |
|
|
|
<div class="mt-4 flex justify-end"> |
|
<button type="button" class="close-modal px-4 py-2 bg-gray-200 text-gray-800 rounded-md mr-2 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"> |
|
Cancel |
|
</button> |
|
<button type="submit" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors"> |
|
Submit Report |
|
</button> |
|
</div> |
|
</div> |
|
</form> |
|
</div> |
|
</div> |
|
{% endif %} |
|
{% endblock %} |
|
|
|
{% block extra_js %} |
|
<script src="{{ url_for('static', filename='js/editor.js') }}"></script> |
|
{% endblock %} |
|
|