Improve moderation dashboard UI and functionality

- Add status tabs (Pending, Approved, Rejected, Escalated)
- Implement HTMX for smooth tab switching and status changes
- Add proper permissions for escalated submissions
- Change Status filter to Submission Type (Text/Photo)
- Move navigation into dashboard content
- Fix tab menu visibility and transitions
- Add contextual loading indicator
- Update styling to match dark theme
- Ensure consistent styling across components
This commit is contained in:
pacnpal
2024-11-13 18:37:36 +00:00
parent 15e56c9770
commit 6a9154ce69
5 changed files with 440 additions and 433 deletions

View File

@@ -1,62 +1,120 @@
{% load static %}
<div class="space-y-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<div class="p-6 transition-shadow duration-200 bg-white border rounded-lg shadow-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 hover:shadow-xl">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Pending Reviews</h3>
<span class="px-3 py-1 text-sm font-medium text-yellow-800 bg-yellow-100 rounded-full dark:bg-yellow-900/50 dark:text-yellow-200">
{{ submissions|length }}
</span>
</div>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Submissions awaiting moderation</p>
</div>
<div class="flex items-center p-4 space-x-4 bg-gray-900 rounded-lg">
<a href="{% url 'moderation:submission_list' %}?status=NEW"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'NEW' or not request.GET.status %}bg-blue-900/40 text-blue-400{% else %}text-gray-400 hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=NEW"
hx-target="#dashboard-content"
hx-push-url="true"
hx-indicator="#loading-indicator">
<i class="mr-2.5 text-lg fas fa-clock"></i>
<span>Pending</span>
</a>
<div class="p-6 transition-shadow duration-200 bg-white border rounded-lg shadow-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 hover:shadow-xl">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Recent Activity</h3>
<i class="text-gray-400 fas fa-history"></i>
</div>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Latest moderation actions</p>
</div>
<a href="{% url 'moderation:submission_list' %}?status=APPROVED"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'APPROVED' %}bg-blue-900/40 text-blue-400{% else %}text-gray-400 hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=APPROVED"
hx-target="#dashboard-content"
hx-push-url="true"
hx-indicator="#loading-indicator">
<i class="mr-2.5 text-lg fas fa-check"></i>
<span>Approved</span>
</a>
<div class="p-6 transition-shadow duration-200 bg-white border rounded-lg shadow-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 hover:shadow-xl">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Quick Actions</h3>
<i class="text-gray-400 fas fa-bolt"></i>
</div>
<div class="mt-4 space-y-2">
<a href="{% url 'moderation:edit_submissions' %}"
class="block px-4 py-2 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
hx-get="{% url 'moderation:edit_submissions' %}"
hx-target="#submissions-content"
hx-push-url="true">
<i class="mr-2 fas fa-edit"></i> Review Edit Submissions
</a>
<a href="{% url 'moderation:photo_submissions' %}"
class="block px-4 py-2 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
hx-get="{% url 'moderation:photo_submissions' %}"
hx-target="#submissions-content"
hx-push-url="true">
<i class="mr-2 fas fa-image"></i> Review Photo Submissions
</a>
</div>
</div>
<a href="{% url 'moderation:submission_list' %}?status=REJECTED"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'REJECTED' %}bg-blue-900/40 text-blue-400{% else %}text-gray-400 hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=REJECTED"
hx-target="#dashboard-content"
hx-push-url="true"
hx-indicator="#loading-indicator">
<i class="mr-2.5 text-lg fas fa-times"></i>
<span>Rejected</span>
</a>
<a href="{% url 'moderation:submission_list' %}?status=ESCALATED"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'ESCALATED' %}bg-blue-900/40 text-blue-400{% else %}text-gray-400 hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=ESCALATED"
hx-target="#dashboard-content"
hx-push-url="true"
hx-indicator="#loading-indicator">
<i class="mr-2.5 text-lg fas fa-exclamation-triangle"></i>
<span>Escalated</span>
</a>
</div>
<div class="bg-white border rounded-lg shadow-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="p-6">
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Recent Submissions</h3>
<div class="space-y-4">
{% for submission in submissions %}
{% include "moderation/partials/submission_list.html" %}
{% empty %}
<div class="py-8 text-center">
<i class="mb-3 text-4xl text-green-500 fas fa-check-circle"></i>
<p class="text-gray-600 dark:text-gray-400">No pending submissions</p>
</div>
{% endfor %}
<div class="p-6 bg-gray-900 rounded-lg">
<form class="flex flex-wrap items-end gap-4 mb-6"
hx-get="{% url 'moderation:submission_list' %}"
hx-target="#dashboard-content"
hx-trigger="change"
hx-push-url="true">
<div class="flex-1 min-w-[200px]">
<label class="block mb-2 text-sm font-medium text-gray-400">
Submission Type
</label>
<select name="submission_type" class="w-full px-3 py-2 text-gray-300 bg-gray-800 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">All Submissions</option>
<option value="text" {% if request.GET.submission_type == 'text' %}selected{% endif %}>Text Submissions</option>
<option value="photo" {% if request.GET.submission_type == 'photo' %}selected{% endif %}>Photo Submissions</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block mb-2 text-sm font-medium text-gray-400">
Type
</label>
<select name="type" class="w-full px-3 py-2 text-gray-300 bg-gray-800 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">All Types</option>
<option value="CREATE" {% if request.GET.type == 'CREATE' %}selected{% endif %}>New Submissions</option>
<option value="EDIT" {% if request.GET.type == 'EDIT' %}selected{% endif %}>Edit Submissions</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block mb-2 text-sm font-medium text-gray-400">
Content Type
</label>
<select name="content_type" class="w-full px-3 py-2 text-gray-300 bg-gray-800 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">All Content</option>
<option value="park" {% if request.GET.content_type == 'park' %}selected{% endif %}>Parks</option>
<option value="ride" {% if request.GET.content_type == 'ride' %}selected{% endif %}>Rides</option>
<option value="company" {% if request.GET.content_type == 'company' %}selected{% endif %}>Companies</option>
</select>
</div>
</form>
<div class="space-y-4">
{% include "moderation/partials/submission_list.html" with submissions=submissions user=user %}
</div>
</div>
</div>
<div id="toast-success"
class="fixed flex items-center w-full max-w-xs p-4 mb-4 text-gray-400 transition-all duration-300 transform translate-y-full bg-gray-800 rounded-lg shadow opacity-0 bottom-4 right-4"
role="alert"
x-data="{ show: false }"
x-show="show"
x-init="$watch('show', value => {
if (value) {
setTimeout(() => show = false, 3000);
}
})"
@htmx:afterOnLoad="show = true"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-full"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-full">
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-400 rounded-lg bg-green-900/40">
<i class="fas fa-check"></i>
</div>
<div class="ml-3 text-sm font-normal" id="toast-message">Status updated successfully</div>
<button type="button"
class="ml-auto -mx-1.5 -my-1.5 text-gray-400 hover:text-gray-300 rounded-lg p-1.5 inline-flex h-8 w-8"
@click="show = false">
<i class="fas fa-times"></i>
</button>
</div>

View File

@@ -1,38 +1,43 @@
{% load static %}
<div class="moderation-nav">
<div class="moderation-nav-container">
<ul>
<li>
<a href="{% url 'moderation:dashboard' %}"
class="{% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
hx-get="{% url 'moderation:dashboard' %}"
hx-target="#submissions-content"
hx-push-url="true">
<i class="fas fa-tachometer-alt"></i>
Dashboard
</a>
</li>
<li>
<a href="{% url 'moderation:edit_submissions' %}"
class="{% if request.resolver_match.url_name == 'edit_submissions' %}active{% endif %}"
hx-get="{% url 'moderation:edit_submissions' %}"
hx-target="#submissions-content"
hx-push-url="true">
<i class="fas fa-edit"></i>
Edit Submissions
</a>
</li>
<li>
<a href="{% url 'moderation:photo_submissions' %}"
class="{% if request.resolver_match.url_name == 'photo_submissions' %}active{% endif %}"
hx-get="{% url 'moderation:photo_submissions' %}"
hx-target="#submissions-content"
hx-push-url="true">
<i class="fas fa-image"></i>
Photo Submissions
</a>
</li>
</ul>
</div>
<div class="flex items-center p-4 space-x-4 bg-gray-900 rounded-lg">
<a href="{% url 'moderation:submission_list' %}?status=NEW"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'NEW' or not request.GET.status %}bg-blue-900/40 text-blue-400{% else %}text-gray-400 hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=NEW"
hx-target="body"
hx-push-url="true"
hx-indicator="#loading-indicator">
<i class="mr-2.5 text-lg fas fa-clock"></i>
<span>Pending</span>
</a>
<a href="{% url 'moderation:submission_list' %}?status=APPROVED"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'APPROVED' %}bg-blue-900/40 text-blue-400{% else %}text-gray-400 hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=APPROVED"
hx-target="body"
hx-push-url="true"
hx-indicator="#loading-indicator">
<i class="mr-2.5 text-lg fas fa-check"></i>
<span>Approved</span>
</a>
<a href="{% url 'moderation:submission_list' %}?status=REJECTED"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'REJECTED' %}bg-blue-900/40 text-blue-400{% else %}text-gray-400 hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=REJECTED"
hx-target="body"
hx-push-url="true"
hx-indicator="#loading-indicator">
<i class="mr-2.5 text-lg fas fa-times"></i>
<span>Rejected</span>
</a>
<a href="{% url 'moderation:submission_list' %}?status=ESCALATED"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'ESCALATED' %}bg-blue-900/40 text-blue-400{% else %}text-gray-400 hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=ESCALATED"
hx-target="body"
hx-push-url="true"
hx-indicator="#loading-indicator">
<i class="mr-2.5 text-lg fas fa-exclamation-triangle"></i>
<span>Escalated</span>
</a>
</div>

View File

@@ -1,137 +1,151 @@
<div id="submissions-content"
class="submission-list"
hx-indicator="#loading-indicator">
{% include "moderation/partials/filters.html" %}
{% for submission in submissions %}
<div class="p-6 submission-card"
id="submission-{{ submission.id }}">
<div class="mb-4 submission-header">
<div>
<h3 class="flex items-center gap-2 text-lg font-semibold text-gray-900 dark:text-white">
<span class="status-badge
{% if submission.status == 'NEW' %}status-pending
{% elif submission.status == 'APPROVED' %}status-approved
{% elif submission.status == 'REJECTED' %}status-rejected
{% elif submission.status == 'ESCALATED' %}status-escalated{% endif %}">
{{ submission.get_status_display }}
</span>
{{ submission.get_content_type_display }} -
{% if submission.submission_type == 'CREATE' %}New{% else %}Edit{% endif %}
</h3>
<div class="mt-1 submission-meta">
<span class="inline-flex items-center">
<i class="mr-1 fas fa-user"></i>
{{ submission.user.username }}
</span>
<span class="mx-2"></span>
<span class="inline-flex items-center">
<i class="mr-1 fas fa-clock"></i>
{{ submission.created_at|date:"M d, Y H:i" }}
</span>
</div>
</div>
</div>
{% if submission.reason %}
<div class="p-4 mt-2 rounded-lg bg-gray-50 dark:bg-gray-700/50">
<div class="text-sm font-medium text-gray-700 dark:text-gray-300">Reason:</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">{{ submission.reason }}</div>
</div>
{% endif %}
{% if submission.source %}
<div class="p-4 mt-2 rounded-lg bg-gray-50 dark:bg-gray-700/50">
<div class="text-sm font-medium text-gray-700 dark:text-gray-300">Source:</div>
<div class="mt-1">
<a href="{{ submission.source }}"
target="_blank"
class="inline-flex items-center text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 hover:underline">
<span>{{ submission.source }}</span>
<i class="ml-1 text-xs fas fa-external-link-alt"></i>
</a>
</div>
</div>
{% endif %}
<div class="mt-4 space-y-3 submission-changes">
{% for field, value in submission.changes.items %}
<div class="p-4 change-item">
<div class="text-sm font-medium text-gray-700 change-label dark:text-gray-300">
{{ field|title }}:
</div>
<div class="mt-1 text-gray-600 change-value dark:text-gray-400">
{{ value }}
</div>
</div>
{% endfor %}
</div>
{% if submission.status == 'NEW' or submission.status == 'ESCALATED' and user.role in 'ADMIN,SUPERUSER' %}
<div class="mt-4 review-notes" x-data="{ showNotes: false }">
<textarea x-show="showNotes"
name="notes"
class="w-full p-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Add review notes (optional)"
rows="3"></textarea>
<div class="flex items-center justify-end gap-3 mt-4 action-buttons">
<button class="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
@click="showNotes = !showNotes">
<i class="mr-2 fas fa-comment-alt"></i>
Add Notes
</button>
<button class="btn-approve"
hx-post="{% url 'moderation:approve_submission' submission.id %}"
hx-target="#submission-{{ submission.id }}"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to approve this submission?"
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-check"></i>
Approve
</button>
<button class="btn-reject"
hx-post="{% url 'moderation:reject_submission' submission.id %}"
hx-target="#submission-{{ submission.id }}"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to reject this submission?"
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-times"></i>
Reject
</button>
{% if user.role == 'MODERATOR' and submission.status != 'ESCALATED' %}
<button class="btn-escalate"
hx-post="{% url 'moderation:escalate_submission' submission.id %}"
hx-target="#submission-{{ submission.id }}"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to escalate this submission?"
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-arrow-up"></i>
Escalate
</button>
{% for submission in submissions %}
<div class="p-6 bg-gray-900 rounded-lg"
id="submission-{{ submission.id }}"
x-data="{ showSuccess: false }"
@htmx:afterOnLoad="showSuccess = true; setTimeout(() => showSuccess = false, 3000)">
<div class="mb-4 submission-header">
<div>
<h3 class="flex items-center gap-3 text-lg font-semibold text-gray-300">
<span class="status-badge
{% if submission.status == 'NEW' %}status-pending
{% elif submission.status == 'APPROVED' %}status-approved
{% elif submission.status == 'REJECTED' %}status-rejected
{% elif submission.status == 'ESCALATED' %}status-escalated{% endif %}">
<i class="mr-1.5 fas fa-{% if submission.status == 'NEW' %}clock
{% elif submission.status == 'APPROVED' %}check
{% elif submission.status == 'REJECTED' %}times
{% elif submission.status == 'ESCALATED' %}exclamation{% endif %}"></i>
{{ submission.get_status_display }}
</span>
{{ submission.get_content_type_display }} -
{% if submission.submission_type == 'CREATE' %}New{% else %}Edit{% endif %}
</h3>
<div class="mt-2 text-gray-400">
<span class="inline-flex items-center">
<i class="mr-1.5 fas fa-user"></i>
{{ submission.user.username }}
</span>
<span class="mx-2.5"></span>
<span class="inline-flex items-center">
<i class="mr-1.5 fas fa-clock"></i>
{{ submission.created_at|date:"M d, Y H:i" }}
</span>
{% if submission.handled_by %}
<span class="mx-2.5"></span>
<span class="inline-flex items-center">
<i class="mr-1.5 fas fa-user-shield"></i>
Handled by {{ submission.handled_by.username }}
</span>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% empty %}
<div class="p-8 text-center bg-white border rounded-lg shadow-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="text-gray-500 dark:text-gray-400">
<i class="mb-3 text-4xl fas fa-inbox"></i>
<p>No submissions found matching your filters.</p>
{% if submission.reason %}
<div class="p-4 mt-3 bg-gray-800 rounded-lg">
<div class="text-sm font-medium text-gray-300">Reason:</div>
<div class="mt-1.5 text-gray-400">{{ submission.reason }}</div>
</div>
{% endif %}
{% if submission.source %}
<div class="p-4 mt-3 bg-gray-800 rounded-lg">
<div class="text-sm font-medium text-gray-300">Source:</div>
<div class="mt-1.5">
<a href="{{ submission.source }}"
target="_blank"
class="inline-flex items-center text-blue-400 hover:text-blue-300">
<span>{{ submission.source }}</span>
<i class="ml-1.5 text-xs fas fa-external-link-alt"></i>
</a>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<div id="loading-indicator"
class="fixed inset-0 flex items-center justify-center htmx-indicator bg-black/20 dark:bg-black/40">
<div class="flex items-center p-6 space-x-4 bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="w-8 h-8 border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
<span class="text-gray-700 dark:text-gray-300">Processing...</span>
<div class="mt-4 space-y-3">
{% for field, value in submission.changes.items %}
<div class="p-4 bg-gray-800 rounded-lg">
<div class="text-sm font-medium text-gray-300">
{{ field|title }}:
</div>
<div class="mt-1.5 text-gray-400">
{{ value }}
</div>
</div>
{% endfor %}
</div>
{% if submission.notes %}
<div class="p-4 mt-4 rounded-lg bg-blue-900/30">
<div class="text-sm font-medium text-blue-300">Review Notes:</div>
<div class="mt-1.5 text-blue-200">{{ submission.notes }}</div>
</div>
{% endif %}
{% if submission.status == 'NEW' or submission.status == 'ESCALATED' and user.role in 'ADMIN,SUPERUSER' %}
<div class="mt-6 review-notes" x-data="{ showNotes: false }">
<div x-show="showNotes"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-2">
<textarea name="notes"
class="w-full px-4 py-3 text-gray-300 bg-gray-800 border border-gray-700 rounded-lg resize-none focus:ring-2 focus:ring-blue-500"
placeholder="Add review notes (optional)"
rows="3"></textarea>
</div>
<div class="flex items-center justify-end gap-3 action-buttons">
<button class="btn-notes"
@click="showNotes = !showNotes">
<i class="mr-2 fas fa-comment-alt"></i>
Add Notes
</button>
{% if submission.status != 'ESCALATED' or user.role in 'ADMIN,SUPERUSER' %}
<button class="btn-approve"
hx-post="{% url 'moderation:approve_submission' submission.id %}"
hx-target="#submissions-content"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to approve this submission?"
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-check"></i>
Approve
</button>
<button class="btn-reject"
hx-post="{% url 'moderation:reject_submission' submission.id %}"
hx-target="#submissions-content"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to reject this submission?"
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-times"></i>
Reject
</button>
{% endif %}
{% if user.role == 'MODERATOR' and submission.status != 'ESCALATED' %}
<button class="btn-escalate"
hx-post="{% url 'moderation:escalate_submission' submission.id %}"
hx-target="#submissions-content"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to escalate this submission?"
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-arrow-up"></i>
Escalate
</button>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% empty %}
<div class="p-8 text-center bg-gray-900 rounded-lg">
<div class="text-gray-400">
<i class="mb-4 text-5xl fas fa-inbox"></i>
<p class="text-lg">No submissions found matching your filters.</p>
</div>
</div>
{% endfor %}