mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:51:09 -05:00
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:
@@ -1,5 +1,5 @@
|
|||||||
from django.views.generic import ListView, TemplateView
|
from django.views.generic import ListView, TemplateView
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.http import HttpResponse, JsonResponse, HttpRequest
|
from django.http import HttpResponse, JsonResponse, HttpRequest
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
@@ -29,91 +29,102 @@ class ModeratorRequiredMixin(UserPassesTestMixin):
|
|||||||
return super().handle_no_permission()
|
return super().handle_no_permission()
|
||||||
raise PermissionDenied("You do not have moderator permissions.")
|
raise PermissionDenied("You do not have moderator permissions.")
|
||||||
|
|
||||||
class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, TemplateView):
|
class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
|
||||||
|
template_name = 'moderation/dashboard.html'
|
||||||
|
context_object_name = 'submissions'
|
||||||
|
paginate_by = 10
|
||||||
|
|
||||||
def get_template_names(self):
|
def get_template_names(self):
|
||||||
if self.request.headers.get('HX-Request'):
|
if self.request.headers.get('HX-Request'):
|
||||||
return ['moderation/partials/dashboard_content.html']
|
return ['moderation/partials/dashboard_content.html']
|
||||||
return ['moderation/dashboard.html']
|
return [self.template_name]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['submissions'] = EditSubmission.objects.all().order_by('-created_at')[:10]
|
|
||||||
return context
|
|
||||||
|
|
||||||
class EditSubmissionListView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
|
|
||||||
context_object_name = 'submissions'
|
|
||||||
|
|
||||||
def get_template_names(self):
|
|
||||||
if self.request.headers.get('HX-Request'):
|
|
||||||
return ['moderation/partials/edit_submission_content.html']
|
|
||||||
return ['moderation/edit_submission_list.html']
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = EditSubmission.objects.all().order_by('-created_at')
|
status = self.request.GET.get('status', 'NEW')
|
||||||
|
submission_type = self.request.GET.get('submission_type', '')
|
||||||
|
|
||||||
if status := self.request.GET.get('status'):
|
if submission_type == 'photo':
|
||||||
queryset = queryset.filter(status=status)
|
queryset = PhotoSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
|
else:
|
||||||
|
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
|
|
||||||
if submission_type := self.request.GET.get('type'):
|
if type_filter := self.request.GET.get('type'):
|
||||||
queryset = queryset.filter(submission_type=submission_type)
|
queryset = queryset.filter(submission_type=type_filter)
|
||||||
|
|
||||||
if content_type := self.request.GET.get('content_type'):
|
if content_type := self.request.GET.get('content_type'):
|
||||||
queryset = queryset.filter(content_type__model=content_type)
|
queryset = queryset.filter(content_type__model=content_type)
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
class PhotoSubmissionListView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
|
|
||||||
context_object_name = 'submissions'
|
|
||||||
|
|
||||||
def get_template_names(self):
|
|
||||||
if self.request.headers.get('HX-Request'):
|
|
||||||
return ['moderation/partials/photo_submission_content.html']
|
|
||||||
return ['moderation/photo_submission_list.html']
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = PhotoSubmission.objects.all().order_by('-created_at')
|
|
||||||
|
|
||||||
if status := self.request.GET.get('status'):
|
|
||||||
queryset = queryset.filter(status=status)
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def submission_list(request: HttpRequest) -> HttpResponse:
|
def submission_list(request: HttpRequest) -> HttpResponse:
|
||||||
"""HTMX endpoint for filtered submission list"""
|
"""View for submission list with filters"""
|
||||||
user = cast(User, request.user)
|
user = cast(User, request.user)
|
||||||
if not (user.role in MODERATOR_ROLES or user.is_superuser):
|
if not (user.role in MODERATOR_ROLES or user.is_superuser):
|
||||||
return HttpResponse(status=403)
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
queryset = EditSubmission.objects.all().order_by('-created_at')
|
status = request.GET.get('status', 'NEW')
|
||||||
|
submission_type = request.GET.get('submission_type', '')
|
||||||
|
|
||||||
if status := request.GET.get('status'):
|
if submission_type == 'photo':
|
||||||
queryset = queryset.filter(status=status)
|
queryset = PhotoSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
if submission_type := request.GET.get('type'):
|
else:
|
||||||
queryset = queryset.filter(submission_type=submission_type)
|
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
if content_type := request.GET.get('content_type'):
|
|
||||||
queryset = queryset.filter(content_type__model=content_type)
|
if type_filter := request.GET.get('type'):
|
||||||
|
queryset = queryset.filter(submission_type=type_filter)
|
||||||
|
|
||||||
|
if content_type := request.GET.get('content_type'):
|
||||||
|
queryset = queryset.filter(content_type__model=content_type)
|
||||||
|
|
||||||
html = render_to_string(
|
context = {
|
||||||
'moderation/partials/submission_list.html',
|
'submissions': queryset,
|
||||||
{'submissions': queryset},
|
'user': request.user,
|
||||||
request=request
|
}
|
||||||
)
|
|
||||||
return HttpResponse(html)
|
# If it's an HTMX request, return just the content
|
||||||
|
if request.headers.get('HX-Request'):
|
||||||
|
return render(request, 'moderation/partials/dashboard_content.html', context)
|
||||||
|
|
||||||
|
# For direct URL access, return the full dashboard template
|
||||||
|
return render(request, 'moderation/dashboard.html', context)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
|
def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
|
||||||
"""HTMX endpoint for approving a submission"""
|
"""HTMX endpoint for approving a submission"""
|
||||||
user = cast(User, request.user)
|
user = cast(User, request.user)
|
||||||
|
submission = get_object_or_404(EditSubmission, id=submission_id)
|
||||||
|
|
||||||
|
if submission.status == 'ESCALATED' and not (user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
|
||||||
|
return HttpResponse("Only admins can handle escalated submissions", status=403)
|
||||||
if not (user.role in MODERATOR_ROLES or user.is_superuser):
|
if not (user.role in MODERATOR_ROLES or user.is_superuser):
|
||||||
return HttpResponse(status=403)
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
submission = get_object_or_404(EditSubmission, id=submission_id)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
submission.approve(user)
|
submission.approve(user)
|
||||||
_update_submission_notes(submission, request.POST.get('notes'))
|
_update_submission_notes(submission, request.POST.get('notes'))
|
||||||
return _render_submission_response('moderation/partials/submission_list.html', submission, request)
|
|
||||||
|
# Get updated queryset with filters
|
||||||
|
status = request.GET.get('status', 'NEW')
|
||||||
|
submission_type = request.GET.get('submission_type', '')
|
||||||
|
|
||||||
|
if submission_type == 'photo':
|
||||||
|
queryset = PhotoSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
|
else:
|
||||||
|
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
|
|
||||||
|
if type_filter := request.GET.get('type'):
|
||||||
|
queryset = queryset.filter(submission_type=type_filter)
|
||||||
|
|
||||||
|
if content_type := request.GET.get('content_type'):
|
||||||
|
queryset = queryset.filter(content_type__model=content_type)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'submissions': queryset,
|
||||||
|
'user': request.user,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'moderation/partials/dashboard_content.html', context)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return HttpResponse(str(e), status=400)
|
return HttpResponse(str(e), status=400)
|
||||||
|
|
||||||
@@ -121,27 +132,73 @@ def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse
|
|||||||
def reject_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
|
def reject_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
|
||||||
"""HTMX endpoint for rejecting a submission"""
|
"""HTMX endpoint for rejecting a submission"""
|
||||||
user = cast(User, request.user)
|
user = cast(User, request.user)
|
||||||
|
submission = get_object_or_404(EditSubmission, id=submission_id)
|
||||||
|
|
||||||
|
if submission.status == 'ESCALATED' and not (user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
|
||||||
|
return HttpResponse("Only admins can handle escalated submissions", status=403)
|
||||||
if not (user.role in MODERATOR_ROLES or user.is_superuser):
|
if not (user.role in MODERATOR_ROLES or user.is_superuser):
|
||||||
return HttpResponse(status=403)
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
submission = get_object_or_404(EditSubmission, id=submission_id)
|
|
||||||
submission.reject(user)
|
submission.reject(user)
|
||||||
_update_submission_notes(submission, request.POST.get('notes'))
|
_update_submission_notes(submission, request.POST.get('notes'))
|
||||||
|
|
||||||
return _render_submission_response('moderation/partials/submission_list.html', submission, request)
|
# Get updated queryset with filters
|
||||||
|
status = request.GET.get('status', 'NEW')
|
||||||
|
submission_type = request.GET.get('submission_type', '')
|
||||||
|
|
||||||
|
if submission_type == 'photo':
|
||||||
|
queryset = PhotoSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
|
else:
|
||||||
|
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
|
|
||||||
|
if type_filter := request.GET.get('type'):
|
||||||
|
queryset = queryset.filter(submission_type=type_filter)
|
||||||
|
|
||||||
|
if content_type := request.GET.get('content_type'):
|
||||||
|
queryset = queryset.filter(content_type__model=content_type)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'submissions': queryset,
|
||||||
|
'user': request.user,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'moderation/partials/dashboard_content.html', context)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def escalate_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
|
def escalate_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
|
||||||
"""HTMX endpoint for escalating a submission"""
|
"""HTMX endpoint for escalating a submission"""
|
||||||
user = cast(User, request.user)
|
user = cast(User, request.user)
|
||||||
if user.role != 'MODERATOR' and not user.is_superuser:
|
if not (user.role in MODERATOR_ROLES or user.is_superuser):
|
||||||
return HttpResponse(status=403)
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
submission = get_object_or_404(EditSubmission, id=submission_id)
|
submission = get_object_or_404(EditSubmission, id=submission_id)
|
||||||
|
if submission.status == 'ESCALATED':
|
||||||
|
return HttpResponse("Submission is already escalated", status=400)
|
||||||
|
|
||||||
submission.escalate(user)
|
submission.escalate(user)
|
||||||
_update_submission_notes(submission, request.POST.get('notes'))
|
_update_submission_notes(submission, request.POST.get('notes'))
|
||||||
|
|
||||||
return _render_submission_response('moderation/partials/submission_list.html', submission, request)
|
# Get updated queryset with filters
|
||||||
|
status = request.GET.get('status', 'NEW')
|
||||||
|
submission_type = request.GET.get('submission_type', '')
|
||||||
|
|
||||||
|
if submission_type == 'photo':
|
||||||
|
queryset = PhotoSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
|
else:
|
||||||
|
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
|
||||||
|
|
||||||
|
if type_filter := request.GET.get('type'):
|
||||||
|
queryset = queryset.filter(submission_type=type_filter)
|
||||||
|
|
||||||
|
if content_type := request.GET.get('content_type'):
|
||||||
|
queryset = queryset.filter(content_type__model=content_type)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'submissions': queryset,
|
||||||
|
'user': request.user,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'moderation/partials/dashboard_content.html', context)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
|
def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
|
||||||
@@ -154,7 +211,7 @@ def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
submission.approve(user, request.POST.get('notes', ''))
|
submission.approve(user, request.POST.get('notes', ''))
|
||||||
return _render_submission_response('moderation/partials/photo_submission.html', submission, request)
|
return render(request, 'moderation/partials/photo_submission.html', {'submission': submission})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return HttpResponse(str(e), status=400)
|
return HttpResponse(str(e), status=400)
|
||||||
|
|
||||||
@@ -168,16 +225,10 @@ def reject_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
|
|||||||
submission = get_object_or_404(PhotoSubmission, id=submission_id)
|
submission = get_object_or_404(PhotoSubmission, id=submission_id)
|
||||||
submission.reject(user, request.POST.get('notes', ''))
|
submission.reject(user, request.POST.get('notes', ''))
|
||||||
|
|
||||||
return _render_submission_response('moderation/partials/photo_submission.html', submission, request)
|
return render(request, 'moderation/partials/photo_submission.html', {'submission': submission})
|
||||||
|
|
||||||
def _update_submission_notes(submission: EditSubmission, notes: Optional[str]) -> None:
|
def _update_submission_notes(submission: EditSubmission, notes: Optional[str]) -> None:
|
||||||
"""Update submission notes if provided."""
|
"""Update submission notes if provided."""
|
||||||
if notes:
|
if notes:
|
||||||
submission.notes = notes
|
submission.notes = notes
|
||||||
submission.save()
|
submission.save()
|
||||||
|
|
||||||
def _render_submission_response(template: str, submission: Any, request: HttpRequest) -> HttpResponse:
|
|
||||||
"""Render submission template response."""
|
|
||||||
context = {'submission': submission}
|
|
||||||
html = render_to_string(template, context, request=request)
|
|
||||||
return HttpResponse(html)
|
|
||||||
|
|||||||
@@ -5,108 +5,24 @@
|
|||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
.moderation-nav {
|
/* Form Elements */
|
||||||
@apply bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shadow-sm sticky top-0 z-10;
|
.form-select {
|
||||||
|
@apply rounded-lg border-gray-700 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 bg-gray-800 text-gray-300 transition-colors duration-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.moderation-nav-container {
|
/* Transitions */
|
||||||
@apply container px-4 mx-auto;
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.moderation-nav ul {
|
.fade-enter-active,
|
||||||
@apply flex items-center h-16 space-x-6;
|
.fade-leave-active {
|
||||||
|
@apply transition-all duration-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.moderation-nav li a {
|
.fade-enter-from,
|
||||||
@apply flex items-center px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200;
|
.fade-leave-to {
|
||||||
}
|
@apply opacity-0;
|
||||||
|
|
||||||
.moderation-nav li a.active {
|
|
||||||
@apply bg-blue-50 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moderation-nav li a i {
|
|
||||||
@apply mr-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moderation-content {
|
|
||||||
@apply container px-4 py-6 mx-auto max-w-6xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submission-list {
|
|
||||||
@apply space-y-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submission-card {
|
|
||||||
@apply bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200/50 dark:border-gray-700/50 transition-all duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submission-card:hover {
|
|
||||||
@apply shadow-xl border-gray-300 dark:border-gray-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submission-header {
|
|
||||||
@apply flex justify-between items-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submission-meta {
|
|
||||||
@apply text-sm text-gray-600 dark:text-gray-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submission-changes {
|
|
||||||
@apply mt-4 space-y-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-item {
|
|
||||||
@apply rounded-lg bg-gray-50 dark:bg-gray-700/50 transition-colors duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-item:hover {
|
|
||||||
@apply bg-gray-100 dark:bg-gray-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-field {
|
|
||||||
@apply font-medium text-gray-900 dark:text-gray-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-value {
|
|
||||||
@apply text-gray-700 dark:text-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
@apply flex justify-end space-x-4 mt-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-approve {
|
|
||||||
@apply px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 transition-colors duration-200 flex items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-reject {
|
|
||||||
@apply px-4 py-2 text-white bg-red-600 rounded-lg hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 transition-colors duration-200 flex items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-escalate {
|
|
||||||
@apply px-4 py-2 text-white bg-yellow-600 rounded-lg hover:bg-yellow-700 dark:bg-yellow-500 dark:hover:bg-yellow-600 transition-colors duration-200 flex items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
@apply px-3 py-1 text-sm rounded-full font-medium inline-flex items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-pending {
|
|
||||||
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-approved, .status-auto_approved {
|
|
||||||
@apply bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-rejected {
|
|
||||||
@apply bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-escalated {
|
|
||||||
@apply bg-orange-100 text-orange-800 dark:bg-orange-900/50 dark:text-orange-200;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HTMX Loading States */
|
/* HTMX Loading States */
|
||||||
@@ -122,34 +38,6 @@
|
|||||||
@apply opacity-0 transition-opacity duration-200;
|
@apply opacity-0 transition-opacity duration-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transitions */
|
|
||||||
[x-cloak] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
@apply transition-opacity duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
@apply opacity-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Elements */
|
|
||||||
.form-select {
|
|
||||||
@apply rounded-lg border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white transition-colors duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters {
|
|
||||||
@apply bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200/50 dark:border-gray-700/50 p-6 transition-all duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters:hover {
|
|
||||||
@apply shadow-xl border-gray-300 dark:border-gray-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
to {
|
||||||
@@ -164,21 +52,19 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include "moderation/partials/moderation_nav.html" %}
|
<div class="container max-w-6xl px-4 py-6 mx-auto">
|
||||||
|
<div id="dashboard-content" class="relative transition-opacity duration-200">
|
||||||
<div class="moderation-content">
|
|
||||||
<div id="submissions-content">
|
|
||||||
{% block moderation_content %}
|
{% block moderation_content %}
|
||||||
{% include "moderation/partials/dashboard_content.html" %}
|
{% include "moderation/partials/dashboard_content.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
|
||||||
</div>
|
<div id="loading-indicator"
|
||||||
|
class="absolute inset-0 flex items-center justify-center rounded-lg htmx-indicator bg-gray-900/80">
|
||||||
<div id="loading-indicator"
|
<div class="flex items-center p-6 space-x-4">
|
||||||
class="fixed inset-0 flex items-center justify-center htmx-indicator bg-black/20 dark:bg-black/40">
|
<div class="w-8 h-8 border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
|
||||||
<div class="flex items-center p-6 space-x-4 bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
<span class="text-gray-300">Loading...</span>
|
||||||
<div class="w-8 h-8 border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
|
</div>
|
||||||
<span class="text-gray-700 dark:text-gray-300">Processing...</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -190,22 +76,15 @@ document.body.addEventListener('htmx:configRequest', function(evt) {
|
|||||||
evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
document.body.addEventListener('htmx:beforeRequest', function(evt) {
|
||||||
if (evt.detail.target.id === 'submissions-content') {
|
if (evt.detail.target.id === 'dashboard-content') {
|
||||||
evt.detail.shouldSwap = true;
|
evt.detail.target.style.opacity = '0.5';
|
||||||
evt.detail.target.classList.add('fade-leave-active');
|
|
||||||
evt.detail.target.classList.add('opacity-0');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
document.body.addEventListener('htmx:afterOnLoad', function(evt) {
|
||||||
if (evt.detail.target.id === 'submissions-content') {
|
if (evt.detail.target.id === 'dashboard-content') {
|
||||||
evt.detail.target.classList.remove('fade-leave-active');
|
evt.detail.target.style.opacity = '1';
|
||||||
evt.detail.target.classList.remove('opacity-0');
|
|
||||||
evt.detail.target.classList.add('fade-enter-active');
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
evt.detail.target.classList.remove('fade-enter-active');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,62 +1,120 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
<div class="flex items-center p-4 space-x-4 bg-gray-900 rounded-lg">
|
||||||
<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">
|
<a href="{% url 'moderation:submission_list' %}?status=NEW"
|
||||||
<div class="flex items-center justify-between">
|
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 %}"
|
||||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Pending Reviews</h3>
|
hx-get="{% url 'moderation:submission_list' %}?status=NEW"
|
||||||
<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">
|
hx-target="#dashboard-content"
|
||||||
{{ submissions|length }}
|
hx-push-url="true"
|
||||||
</span>
|
hx-indicator="#loading-indicator">
|
||||||
</div>
|
<i class="mr-2.5 text-lg fas fa-clock"></i>
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Submissions awaiting moderation</p>
|
<span>Pending</span>
|
||||||
</div>
|
</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">
|
<a href="{% url 'moderation:submission_list' %}?status=APPROVED"
|
||||||
<div class="flex items-center justify-between">
|
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 %}"
|
||||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Recent Activity</h3>
|
hx-get="{% url 'moderation:submission_list' %}?status=APPROVED"
|
||||||
<i class="text-gray-400 fas fa-history"></i>
|
hx-target="#dashboard-content"
|
||||||
</div>
|
hx-push-url="true"
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Latest moderation actions</p>
|
hx-indicator="#loading-indicator">
|
||||||
</div>
|
<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">
|
<a href="{% url 'moderation:submission_list' %}?status=REJECTED"
|
||||||
<div class="flex items-center justify-between">
|
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 %}"
|
||||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Quick Actions</h3>
|
hx-get="{% url 'moderation:submission_list' %}?status=REJECTED"
|
||||||
<i class="text-gray-400 fas fa-bolt"></i>
|
hx-target="#dashboard-content"
|
||||||
</div>
|
hx-push-url="true"
|
||||||
<div class="mt-4 space-y-2">
|
hx-indicator="#loading-indicator">
|
||||||
<a href="{% url 'moderation:edit_submissions' %}"
|
<i class="mr-2.5 text-lg fas fa-times"></i>
|
||||||
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"
|
<span>Rejected</span>
|
||||||
hx-get="{% url 'moderation:edit_submissions' %}"
|
</a>
|
||||||
hx-target="#submissions-content"
|
|
||||||
hx-push-url="true">
|
<a href="{% url 'moderation:submission_list' %}?status=ESCALATED"
|
||||||
<i class="mr-2 fas fa-edit"></i> Review Edit Submissions
|
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 %}"
|
||||||
</a>
|
hx-get="{% url 'moderation:submission_list' %}?status=ESCALATED"
|
||||||
<a href="{% url 'moderation:photo_submissions' %}"
|
hx-target="#dashboard-content"
|
||||||
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-push-url="true"
|
||||||
hx-get="{% url 'moderation:photo_submissions' %}"
|
hx-indicator="#loading-indicator">
|
||||||
hx-target="#submissions-content"
|
<i class="mr-2.5 text-lg fas fa-exclamation-triangle"></i>
|
||||||
hx-push-url="true">
|
<span>Escalated</span>
|
||||||
<i class="mr-2 fas fa-image"></i> Review Photo Submissions
|
</a>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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 bg-gray-900 rounded-lg">
|
||||||
<div class="p-6">
|
<form class="flex flex-wrap items-end gap-4 mb-6"
|
||||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Recent Submissions</h3>
|
hx-get="{% url 'moderation:submission_list' %}"
|
||||||
<div class="space-y-4">
|
hx-target="#dashboard-content"
|
||||||
{% for submission in submissions %}
|
hx-trigger="change"
|
||||||
{% include "moderation/partials/submission_list.html" %}
|
hx-push-url="true">
|
||||||
{% empty %}
|
|
||||||
<div class="py-8 text-center">
|
<div class="flex-1 min-w-[200px]">
|
||||||
<i class="mb-3 text-4xl text-green-500 fas fa-check-circle"></i>
|
<label class="block mb-2 text-sm font-medium text-gray-400">
|
||||||
<p class="text-gray-600 dark:text-gray-400">No pending submissions</p>
|
Submission Type
|
||||||
</div>
|
</label>
|
||||||
{% endfor %}
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
</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>
|
||||||
|
|||||||
@@ -1,38 +1,43 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<div class="moderation-nav">
|
<div class="flex items-center p-4 space-x-4 bg-gray-900 rounded-lg">
|
||||||
<div class="moderation-nav-container">
|
<a href="{% url 'moderation:submission_list' %}?status=NEW"
|
||||||
<ul>
|
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 %}"
|
||||||
<li>
|
hx-get="{% url 'moderation:submission_list' %}?status=NEW"
|
||||||
<a href="{% url 'moderation:dashboard' %}"
|
hx-target="body"
|
||||||
class="{% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
|
hx-push-url="true"
|
||||||
hx-get="{% url 'moderation:dashboard' %}"
|
hx-indicator="#loading-indicator">
|
||||||
hx-target="#submissions-content"
|
<i class="mr-2.5 text-lg fas fa-clock"></i>
|
||||||
hx-push-url="true">
|
<span>Pending</span>
|
||||||
<i class="fas fa-tachometer-alt"></i>
|
</a>
|
||||||
Dashboard
|
|
||||||
</a>
|
<a href="{% url 'moderation:submission_list' %}?status=APPROVED"
|
||||||
</li>
|
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 %}"
|
||||||
<li>
|
hx-get="{% url 'moderation:submission_list' %}?status=APPROVED"
|
||||||
<a href="{% url 'moderation:edit_submissions' %}"
|
hx-target="body"
|
||||||
class="{% if request.resolver_match.url_name == 'edit_submissions' %}active{% endif %}"
|
hx-push-url="true"
|
||||||
hx-get="{% url 'moderation:edit_submissions' %}"
|
hx-indicator="#loading-indicator">
|
||||||
hx-target="#submissions-content"
|
<i class="mr-2.5 text-lg fas fa-check"></i>
|
||||||
hx-push-url="true">
|
<span>Approved</span>
|
||||||
<i class="fas fa-edit"></i>
|
</a>
|
||||||
Edit Submissions
|
|
||||||
</a>
|
<a href="{% url 'moderation:submission_list' %}?status=REJECTED"
|
||||||
</li>
|
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 %}"
|
||||||
<li>
|
hx-get="{% url 'moderation:submission_list' %}?status=REJECTED"
|
||||||
<a href="{% url 'moderation:photo_submissions' %}"
|
hx-target="body"
|
||||||
class="{% if request.resolver_match.url_name == 'photo_submissions' %}active{% endif %}"
|
hx-push-url="true"
|
||||||
hx-get="{% url 'moderation:photo_submissions' %}"
|
hx-indicator="#loading-indicator">
|
||||||
hx-target="#submissions-content"
|
<i class="mr-2.5 text-lg fas fa-times"></i>
|
||||||
hx-push-url="true">
|
<span>Rejected</span>
|
||||||
<i class="fas fa-image"></i>
|
</a>
|
||||||
Photo Submissions
|
|
||||||
</a>
|
<a href="{% url 'moderation:submission_list' %}?status=ESCALATED"
|
||||||
</li>
|
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 %}"
|
||||||
</ul>
|
hx-get="{% url 'moderation:submission_list' %}?status=ESCALATED"
|
||||||
</div>
|
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>
|
</div>
|
||||||
|
|||||||
@@ -1,137 +1,151 @@
|
|||||||
<div id="submissions-content"
|
{% for submission in submissions %}
|
||||||
class="submission-list"
|
<div class="p-6 bg-gray-900 rounded-lg"
|
||||||
hx-indicator="#loading-indicator">
|
id="submission-{{ submission.id }}"
|
||||||
|
x-data="{ showSuccess: false }"
|
||||||
{% include "moderation/partials/filters.html" %}
|
@htmx:afterOnLoad="showSuccess = true; setTimeout(() => showSuccess = false, 3000)">
|
||||||
|
<div class="mb-4 submission-header">
|
||||||
{% for submission in submissions %}
|
<div>
|
||||||
<div class="p-6 submission-card"
|
<h3 class="flex items-center gap-3 text-lg font-semibold text-gray-300">
|
||||||
id="submission-{{ submission.id }}">
|
<span class="status-badge
|
||||||
<div class="mb-4 submission-header">
|
{% if submission.status == 'NEW' %}status-pending
|
||||||
<div>
|
{% elif submission.status == 'APPROVED' %}status-approved
|
||||||
<h3 class="flex items-center gap-2 text-lg font-semibold text-gray-900 dark:text-white">
|
{% elif submission.status == 'REJECTED' %}status-rejected
|
||||||
<span class="status-badge
|
{% elif submission.status == 'ESCALATED' %}status-escalated{% endif %}">
|
||||||
{% if submission.status == 'NEW' %}status-pending
|
<i class="mr-1.5 fas fa-{% if submission.status == 'NEW' %}clock
|
||||||
{% elif submission.status == 'APPROVED' %}status-approved
|
{% elif submission.status == 'APPROVED' %}check
|
||||||
{% elif submission.status == 'REJECTED' %}status-rejected
|
{% elif submission.status == 'REJECTED' %}times
|
||||||
{% elif submission.status == 'ESCALATED' %}status-escalated{% endif %}">
|
{% elif submission.status == 'ESCALATED' %}exclamation{% endif %}"></i>
|
||||||
{{ submission.get_status_display }}
|
{{ submission.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
{{ submission.get_content_type_display }} -
|
{{ submission.get_content_type_display }} -
|
||||||
{% if submission.submission_type == 'CREATE' %}New{% else %}Edit{% endif %}
|
{% if submission.submission_type == 'CREATE' %}New{% else %}Edit{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-1 submission-meta">
|
<div class="mt-2 text-gray-400">
|
||||||
<span class="inline-flex items-center">
|
<span class="inline-flex items-center">
|
||||||
<i class="mr-1 fas fa-user"></i>
|
<i class="mr-1.5 fas fa-user"></i>
|
||||||
{{ submission.user.username }}
|
{{ submission.user.username }}
|
||||||
</span>
|
</span>
|
||||||
<span class="mx-2">•</span>
|
<span class="mx-2.5">•</span>
|
||||||
<span class="inline-flex items-center">
|
<span class="inline-flex items-center">
|
||||||
<i class="mr-1 fas fa-clock"></i>
|
<i class="mr-1.5 fas fa-clock"></i>
|
||||||
{{ submission.created_at|date:"M d, Y H:i" }}
|
{{ submission.created_at|date:"M d, Y H:i" }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
{% if submission.handled_by %}
|
||||||
</div>
|
<span class="mx-2.5">•</span>
|
||||||
</div>
|
<span class="inline-flex items-center">
|
||||||
|
<i class="mr-1.5 fas fa-user-shield"></i>
|
||||||
{% if submission.reason %}
|
Handled by {{ submission.handled_by.username }}
|
||||||
<div class="p-4 mt-2 rounded-lg bg-gray-50 dark:bg-gray-700/50">
|
</span>
|
||||||
<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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</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">
|
{% if submission.reason %}
|
||||||
<div class="text-gray-500 dark:text-gray-400">
|
<div class="p-4 mt-3 bg-gray-800 rounded-lg">
|
||||||
<i class="mb-3 text-4xl fas fa-inbox"></i>
|
<div class="text-sm font-medium text-gray-300">Reason:</div>
|
||||||
<p>No submissions found matching your filters.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="loading-indicator"
|
<div class="mt-4 space-y-3">
|
||||||
class="fixed inset-0 flex items-center justify-center htmx-indicator bg-black/20 dark:bg-black/40">
|
{% for field, value in submission.changes.items %}
|
||||||
<div class="flex items-center p-6 space-x-4 bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
<div class="p-4 bg-gray-800 rounded-lg">
|
||||||
<div class="w-8 h-8 border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
|
<div class="text-sm font-medium text-gray-300">
|
||||||
<span class="text-gray-700 dark:text-gray-300">Processing...</span>
|
{{ 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>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user