Revert "update"

This reverts commit 75cc618c2b.
This commit is contained in:
pacnpal
2025-09-21 20:11:00 -04:00
parent 75cc618c2b
commit 540f40e689
610 changed files with 4812 additions and 1715 deletions

View File

@@ -1,297 +0,0 @@
{% extends "base/base.html" %}
{% load static %}
{% block title %}ThrillWiki Moderation{% endblock %}
{% block extra_css %}
<style>
/* Base Styles */
:root {
--loading-gradient: linear-gradient(90deg, var(--tw-gradient-from) 0%, var(--tw-gradient-to) 50%, var(--tw-gradient-from) 100%);
}
/* Responsive Layout */
@media (max-width: 768px) {
.grid-cols-responsive {
@apply grid-cols-1;
}
.action-buttons {
@apply flex-col;
}
.action-buttons > * {
@apply w-full justify-center;
}
}
/* Form Elements */
.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;
}
/* State Management */
[x-cloak] {
display: none !important;
}
/* Loading States */
.htmx-request .htmx-indicator {
@apply opacity-100;
}
.htmx-request.htmx-indicator {
@apply opacity-100;
}
.htmx-indicator {
@apply opacity-0 transition-opacity duration-200;
}
/* Skeleton Loading Animation */
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
/* Transitions */
.fade-enter-active,
.fade-leave-active {
@apply transition-all duration-200;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
/* Custom Animations */
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}
.animate-shimmer {
position: relative;
overflow: hidden;
}
.animate-shimmer::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background-image: var(--loading-gradient);
animation: shimmer 2s infinite;
content: '';
}
/* Accessibility Enhancements */
:focus-visible {
@apply outline-hidden ring-2 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-gray-900;
}
@media (prefers-reduced-motion: reduce) {
.animate-shimmer::after {
animation: none;
}
.animate-pulse {
animation: none;
}
}
/* Touch Device Optimizations */
@media (hover: none) {
.hover\:shadow-md {
@apply shadow-xs;
}
.action-buttons > * {
@apply active:transform active:scale-95;
}
}
/* Dark Mode Optimizations */
.dark .animate-shimmer::after {
--tw-gradient-from: rgba(31, 41, 55, 0);
--tw-gradient-to: rgba(31, 41, 55, 0.1);
}
/* Error States */
.error-shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
}
@keyframes shake {
10%, 90% { transform: translate3d(-1px, 0, 0); }
20%, 80% { transform: translate3d(2px, 0, 0); }
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
40%, 60% { transform: translate3d(4px, 0, 0); }
}
</style>
{% endblock %}
{% block content %}
<div class="container max-w-6xl px-4 py-6 mx-auto">
<div id="dashboard-content" class="relative transition-all duration-200">
{% block moderation_content %}
{% include "moderation/partials/dashboard_content.html" %}
{% endblock %}
<!-- Loading Skeleton -->
<div class="absolute inset-0 htmx-indicator" id="loading-skeleton">
{% include "moderation/partials/loading_skeleton.html" %}
</div>
<!-- Error State -->
<div class="absolute inset-0 hidden" id="error-state">
<div class="flex flex-col items-center justify-center h-full p-6 space-y-4 text-center">
<div class="p-4 text-red-500 bg-red-100 rounded-full dark:bg-red-900/40">
<i class="text-4xl fas fa-exclamation-circle"></i>
</div>
<h3 class="text-lg font-medium text-red-600 dark:text-red-400">
Something went wrong
</h3>
<p class="max-w-md text-gray-600 dark:text-gray-400" id="error-message">
There was a problem loading the content. Please try again.
</p>
<button class="px-4 py-2 font-medium text-white transition-all duration-200 bg-red-600 rounded-lg hover:bg-red-500 dark:bg-red-700 dark:hover:bg-red-600"
onclick="window.location.reload()">
<i class="mr-2 fas fa-sync-alt"></i>
Retry
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// HTMX Configuration and Enhancements
document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
});
// Loading and Error State Management
const dashboard = {
content: document.getElementById('dashboard-content'),
skeleton: document.getElementById('loading-skeleton'),
errorState: document.getElementById('error-state'),
errorMessage: document.getElementById('error-message'),
showLoading() {
this.content.setAttribute('aria-busy', 'true');
this.content.style.opacity = '0';
this.errorState.classList.add('hidden');
},
hideLoading() {
this.content.setAttribute('aria-busy', 'false');
this.content.style.opacity = '1';
},
showError(message) {
this.errorState.classList.remove('hidden');
this.errorMessage.textContent = message || 'There was a problem loading the content. Please try again.';
// Announce error to screen readers
this.errorMessage.setAttribute('role', 'alert');
}
};
// Enhanced HTMX Event Handlers
document.body.addEventListener('htmx:beforeRequest', function(evt) {
if (evt.detail.target.id === 'dashboard-content') {
dashboard.showLoading();
}
});
document.body.addEventListener('htmx:afterOnLoad', function(evt) {
if (evt.detail.target.id === 'dashboard-content') {
dashboard.hideLoading();
// Reset focus for accessibility
const firstFocusable = evt.detail.target.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (firstFocusable) {
firstFocusable.focus();
}
}
});
document.body.addEventListener('htmx:responseError', function(evt) {
if (evt.detail.target.id === 'dashboard-content') {
dashboard.showError(evt.detail.error);
}
});
// Search Input Debouncing
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Apply debouncing to search inputs
document.querySelectorAll('[data-search]').forEach(input => {
const originalSearch = () => {
htmx.trigger(input, 'input');
};
const debouncedSearch = debounce(originalSearch, 300);
input.addEventListener('input', (e) => {
e.preventDefault();
debouncedSearch();
});
});
// Virtual Scrolling for Large Lists
const observerOptions = {
root: null,
rootMargin: '100px',
threshold: 0.1
};
const loadMoreContent = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting && !entry.target.classList.contains('loading')) {
entry.target.classList.add('loading');
htmx.trigger(entry.target, 'intersect');
}
});
};
const observer = new IntersectionObserver(loadMoreContent, observerOptions);
document.querySelectorAll('[data-infinite-scroll]').forEach(el => observer.observe(el));
// Keyboard Navigation Enhancement
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const openModals = document.querySelectorAll('[x-show="showNotes"]');
openModals.forEach(modal => {
const alpineData = modal.__x.$data;
if (alpineData.showNotes) {
alpineData.showNotes = false;
}
});
}
});
</script>
{% endblock %}

View File

@@ -1,7 +0,0 @@
{% extends "moderation/dashboard.html" %}
{% block moderation_content %}
<div id="submissions-content">
{% include "moderation/partials/edit_submission_content.html" %}
</div>
{% endblock %}

View File

@@ -1,127 +0,0 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Moderation - ThrillWiki{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h1 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">Moderation Queue</h1>
<!-- Tabs -->
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px" role="tablist">
<li class="mr-2">
<button class="tab-button {% if active_tab == 'new' %}active{% endif %}"
data-tab="new"
hx-get="{% url 'moderation:edit_submissions' %}?tab=new"
hx-target="#submissions-content"
hx-push-url="true">
New
{% if new_count %}<span class="ml-2 badge">{{ new_count }}</span>{% endif %}
</button>
</li>
<li class="mr-2">
<button class="tab-button {% if active_tab == 'approved' %}active{% endif %}"
data-tab="approved"
hx-get="{% url 'moderation:edit_submissions' %}?tab=approved"
hx-target="#submissions-content"
hx-push-url="true">
Approved
</button>
</li>
<li class="mr-2">
<button class="tab-button {% if active_tab == 'rejected' %}active{% endif %}"
data-tab="rejected"
hx-get="{% url 'moderation:edit_submissions' %}?tab=rejected"
hx-target="#submissions-content"
hx-push-url="true">
Rejected
</button>
</li>
{% if user.role == 'ADMIN' or user.role == 'SUPERUSER' %}
<li>
<button class="tab-button {% if active_tab == 'escalated' %}active{% endif %}"
data-tab="escalated"
hx-get="{% url 'moderation:edit_submissions' %}?tab=escalated"
hx-target="#submissions-content"
hx-push-url="true">
Escalated
{% if escalated_count %}<span class="ml-2 badge">{{ escalated_count }}</span>{% endif %}
</button>
</li>
{% endif %}
</ul>
</div>
<!-- Tab Content -->
<div id="submissions-content">
{% include 'moderation/partials/submission_list.html' %}
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.tab-button {
@apply inline-flex items-center px-4 py-2 text-sm font-medium border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300;
}
.tab-button.active {
@apply text-blue-600 border-blue-600 dark:text-blue-500 dark:border-blue-500;
}
.badge {
@apply px-2 py-1 text-xs font-semibold text-white bg-blue-500 rounded-full;
}
.submission-card {
@apply p-4 mb-4 bg-white border rounded-lg shadow dark:bg-gray-700 dark:border-gray-600;
}
.submission-header {
@apply flex items-center justify-between mb-2;
}
.submission-title {
@apply text-lg font-semibold text-gray-900 dark:text-white;
}
.submission-meta {
@apply text-sm text-gray-500 dark:text-gray-400;
}
.submission-changes {
@apply mt-4 space-y-2;
}
.change-item {
@apply flex items-start;
}
.change-label {
@apply w-32 font-medium text-gray-700 dark:text-gray-300;
}
.change-value {
@apply flex-1 text-gray-900 dark:text-white;
}
.action-buttons {
@apply flex gap-2 mt-4;
}
.btn-approve {
@apply px-4 py-2 text-white bg-green-500 rounded-lg hover:bg-green-600;
}
.btn-reject {
@apply px-4 py-2 text-white bg-red-500 rounded-lg hover:bg-red-600;
}
.btn-escalate {
@apply px-4 py-2 text-white bg-yellow-500 rounded-lg hover:bg-yellow-600;
}
</style>
{% endblock %}

View File

@@ -1,135 +0,0 @@
{% load moderation_tags %}
<div class="p-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<h3 class="mb-4 text-lg font-semibold">Coaster Stats</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Height (ft):
</label>
<input type="number"
name="stats.height_ft"
value="{{ stats.height_ft }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Height in feet">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Length (ft):
</label>
<input type="number"
name="stats.length_ft"
value="{{ stats.length_ft }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Length in feet">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Speed (mph):
</label>
<input type="number"
name="stats.speed_mph"
value="{{ stats.speed_mph }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Speed in mph">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Inversions:
</label>
<input type="number"
name="stats.inversions"
value="{{ stats.inversions }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Number of inversions">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Launch Type:
</label>
<select name="stats.launch_type"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500">
<option value="">Select launch type</option>
<option value="CHAIN_LIFT" {% if stats.launch_type == 'CHAIN_LIFT' %}selected{% endif %}>Chain Lift</option>
<option value="LSM" {% if stats.launch_type == 'LSM' %}selected{% endif %}>LSM</option>
<option value="HYDRAULIC" {% if stats.launch_type == 'HYDRAULIC' %}selected{% endif %}>Hydraulic</option>
<option value="TIRE_DRIVE" {% if stats.launch_type == 'TIRE_DRIVE' %}selected{% endif %}>Tire Drive</option>
<option value="CABLE_LIFT" {% if stats.launch_type == 'CABLE_LIFT' %}selected{% endif %}>Cable Lift</option>
<option value="OTHER" {% if stats.launch_type == 'OTHER' %}selected{% endif %}>Other</option>
</select>
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Track Material:
</label>
<select name="stats.track_material"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500">
<option value="">Select track material</option>
<option value="STEEL" {% if stats.track_material == 'STEEL' %}selected{% endif %}>Steel</option>
<option value="WOOD" {% if stats.track_material == 'WOOD' %}selected{% endif %}>Wood</option>
<option value="HYBRID" {% if stats.track_material == 'HYBRID' %}selected{% endif %}>Hybrid</option>
</select>
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Coaster Type:
</label>
<select name="stats.roller_coaster_type"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500">
<option value="">Select coaster type</option>
<option value="SIT_DOWN" {% if stats.roller_coaster_type == 'SIT_DOWN' %}selected{% endif %}>Sit Down</option>
<option value="INVERTED" {% if stats.roller_coaster_type == 'INVERTED' %}selected{% endif %}>Inverted</option>
<option value="FLYING" {% if stats.roller_coaster_type == 'FLYING' %}selected{% endif %}>Flying</option>
<option value="STAND_UP" {% if stats.roller_coaster_type == 'STAND_UP' %}selected{% endif %}>Stand Up</option>
<option value="WING" {% if stats.roller_coaster_type == 'WING' %}selected{% endif %}>Wing</option>
<option value="OTHER" {% if stats.roller_coaster_type == 'OTHER' %}selected{% endif %}>Other</option>
</select>
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Number of Trains:
</label>
<input type="number"
name="stats.trains_count"
value="{{ stats.trains_count }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Number of trains">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Cars per Train:
</label>
<input type="number"
name="stats.cars_per_train"
value="{{ stats.cars_per_train }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Cars per train">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
Seats per Car:
</label>
<input type="number"
name="stats.seats_per_car"
value="{{ stats.seats_per_car }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Seats per car">
</div>
</div>
</div>

View File

@@ -1,222 +0,0 @@
{% load static %}
<h1 class="mb-6 text-2xl font-bold text-gray-900 dark:text-gray-200">Moderation Dashboard</h1>
<div class="space-y-6">
<div class="flex items-center justify-between p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="flex items-center space-x-4">
<a href="{% url 'moderation:submission_list' %}?status=PENDING"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'PENDING' or not request.GET.status %}bg-blue-100 text-blue-900 dark:bg-blue-900/40 dark:text-blue-400{% else %}text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=PENDING"
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>
<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-100 text-blue-900 dark:bg-blue-900/40 dark:text-blue-400{% else %}text-gray-600 dark:text-gray-400 hover:text-gray-900 dark: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>
<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-100 text-blue-900 dark:bg-blue-900/40 dark:text-blue-400{% else %}text-gray-600 dark:text-gray-400 hover:text-gray-900 dark: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-100 text-blue-900 dark:bg-blue-900/40 dark:text-blue-400{% else %}text-gray-600 dark:text-gray-400 hover:text-gray-900 dark: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>
<button class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 text-gray-600 dark:text-gray-400 hover:text-blue-900 hover:bg-blue-100 dark:hover:text-blue-400 dark:hover:bg-blue-900/40"
hx-get="{{ request.get_full_path }}"
hx-target="#dashboard-content"
hx-indicator="#loading-indicator">
<i class="mr-2.5 text-lg fas fa-sync-alt"></i>
<span>Refresh</span>
</button>
</div>
<div class="p-6 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<form class="mb-6"
x-data="{ showFilters: false }"
hx-get="{% url 'moderation:submission_list' %}"
hx-target="#dashboard-content"
hx-trigger="change from:select"
hx-push-url="true"
aria-label="Submission filters">
<!-- Mobile Filter Toggle -->
<button type="button"
class="flex items-center w-full gap-2 p-3 mb-4 font-medium text-left text-gray-700 transition-colors duration-200 bg-gray-100 rounded-lg md:hidden dark:text-gray-300 dark:bg-gray-900"
@click="showFilters = !showFilters"
:aria-expanded="showFilters"
aria-controls="filter-controls">
<i class="fas" :class="showFilters ? 'fa-chevron-up' : 'fa-chevron-down'"></i>
<span>Filter Options</span>
<span class="flex items-center ml-auto space-x-1 text-sm text-gray-500 dark:text-gray-400">
<span class="active-filters">{{ request.GET|length }} active</span>
</span>
</button>
<!-- Filter Controls -->
<div id="filter-controls"
class="grid gap-4 transition-all duration-200 md:grid-cols-3"
:class="{'hidden md:grid': !showFilters, 'grid': showFilters}"
role="group"
aria-label="Filter controls">
<div class="relative">
<label id="submission-type-label"
class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Submission Type
</label>
<select name="submission_type"
aria-labelledby="submission-type-label"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 focus:outline-hidden focus:ring-2 focus:ring-blue-500"
@change="$dispatch('filter-changed')">
<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="relative">
<label id="type-label"
class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Type
</label>
<select name="type"
aria-labelledby="type-label"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 focus:outline-hidden focus:ring-2 focus:ring-blue-500"
@change="$dispatch('filter-changed')">
<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="relative">
<label id="content-type-label"
class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Content Type
</label>
<select name="content_type"
aria-labelledby="content-type-label"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 focus:outline-hidden focus:ring-2 focus:ring-blue-500"
@change="$dispatch('filter-changed')">
<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>
<!-- Applied Filters Summary -->
<div class="hidden pt-2 md:col-span-3 md:block">
<div class="flex flex-wrap gap-2"
x-show="$store.filters.hasActiveFilters"
x-cloak>
<template x-for="filter in $store.filters.active" :key="filter.name">
<span class="inline-flex items-center px-2 py-1 text-sm bg-blue-100 rounded dark:bg-blue-900/40">
<span class="mr-1 text-blue-700 dark:text-blue-300" x-text="filter.label + ':'"></span>
<span class="font-medium text-blue-900 dark:text-blue-100" x-text="filter.value"></span>
</span>
</template>
</div>
</div>
</div>
</form>
<div class="space-y-4">
{% include "moderation/partials/submission_list.html" with submissions=submissions user=user %}
</div>
</div>
</div>
<div id="toast-container"
x-data="{
show: false,
message: '',
icon: 'check',
color: 'green',
showToast(msg, icn = 'check', clr = 'green') {
this.message = msg;
this.icon = icn;
this.color = clr;
this.show = true;
setTimeout(() => this.show = false, 3000);
}
}"
@submission-approved.window="showToast('Submission approved successfully', 'check', 'green')"
@submission-rejected.window="showToast('Submission rejected', 'times', 'red')"
@submission-escalated.window="showToast('Submission escalated', 'exclamation-triangle', 'yellow')"
@submission-updated.window="showToast('Changes saved successfully', 'check', 'blue')"
class="fixed z-50 bottom-4 right-4">
<div x-show="show"
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"
class="flex items-center w-full max-w-xs p-4 text-gray-400 bg-gray-800 rounded-lg shadow">
<div class="inline-flex items-center justify-center shrink-0 w-8 h-8 rounded-lg"
:class="{
'text-green-400 bg-green-900/40': color === 'green',
'text-red-400 bg-red-900/40': color === 'red',
'text-yellow-400 bg-yellow-900/40': color === 'yellow',
'text-blue-400 bg-blue-900/40': color === 'blue'
}">
<i class="fas" :class="'fa-' + icon"></i>
</div>
<div class="ml-3 text-sm font-normal" x-text="message"></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>
</div>
<script>
document.body.addEventListener('htmx:afterRequest', function(evt) {
if (evt.detail.successful) {
const path = evt.detail.requestConfig.path;
let event;
if (path.includes('approve')) {
event = new CustomEvent('submission-approved');
} else if (path.includes('reject')) {
event = new CustomEvent('submission-rejected');
} else if (path.includes('escalate')) {
event = new CustomEvent('submission-escalated');
} else if (path.includes('edit')) {
event = new CustomEvent('submission-updated');
}
if (event) {
window.dispatchEvent(event);
}
}
});
</script>

View File

@@ -1,67 +0,0 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
{% if designers %}
{% for designer in designers %}
<button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectDesignerForSubmission('{{ designer.id }}', '{{ designer.name|escapejs }}', '{{ submission_id }}')">
{{ designer.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No designers found
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectDesignerForSubmission(id, name, submissionId) {
// Debug logging
console.log('Selecting designer:', {id, name, submissionId});
// Find elements
const designerInput = document.querySelector(`#designer-input-${submissionId}`);
const searchInput = document.querySelector(`#designer-search-${submissionId}`);
const resultsDiv = document.querySelector(`#designer-search-results-${submissionId}`);
// Debug logging
console.log('Found elements:', {
designerInput: designerInput?.id,
searchInput: searchInput?.id,
resultsDiv: resultsDiv?.id
});
// Update hidden input
if (designerInput) {
designerInput.value = id;
console.log('Updated designer input value:', designerInput.value);
}
// Update search input
if (searchInput) {
searchInput.value = name;
console.log('Updated search input value:', searchInput.value);
}
// Clear results
if (resultsDiv) {
resultsDiv.innerHTML = '';
console.log('Cleared results div');
}
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="designer-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#designer-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
}
});
});
</script>

View File

@@ -1,2 +0,0 @@
{% include "moderation/partials/filters.html" %}
{% include "moderation/partials/submission_list.html" %}

View File

@@ -1,132 +0,0 @@
{% load moderation_tags %}
<div class="p-6 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50"
id="edit-form-{{ submission.id }}">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">
Edit Submission
</h3>
<form hx-post="{% url 'moderation:edit_submission' submission.id %}"
hx-target="#submissions-content"
class="space-y-4">
{% for field, value in changes.items %}
{% if field != 'model_name' %}
<div class="p-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
{% if field == 'stats' %}
Coaster Stats:
{% elif field == 'park_area' %}
Park Area:
{% elif field == 'ride_model' %}
Ride Model:
{% elif field == 'min_height_in' %}
Minimum Height:
{% elif field == 'max_height_in' %}
Maximum Height:
{% elif field == 'capacity_per_hour' %}
Hourly Capacity:
{% elif field == 'ride_duration_seconds' %}
Ride Duration:
{% elif field == 'opening_date' %}
Opening Date:
{% elif field == 'closing_date' %}
Closing Date:
{% elif field == 'status_since' %}
Status Since:
{% elif field == 'operating_season' %}
Operating Season:
{% elif field == 'size_acres' %}
Size (Acres):
{% elif field == 'post_closing_status' %}
Post-Closing Status:
{% else %}
{{ field|title }}:
{% endif %}
</label>
{% if field == 'stats' %}
<div class="space-y-2">
{% for stat_name, stat_value in value.items %}
<div class="flex items-center gap-2">
<label class="text-sm text-gray-700 dark:text-gray-400">{{ stat_name|title }}:</label>
<input type="text"
name="stats.{{ stat_name }}"
value="{{ stat_value }}"
class="flex-1 px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
</div>
{% endfor %}
</div>
{% elif field == 'park' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
{% for park_id, park_name in parks %}
<option value="{{ park_id }}" {% if park_id == value %}selected{% endif %}>{{ park_name }}</option>
{% endfor %}
</select>
{% elif field == 'designer' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<option value="">None</option>
{% for designer_id, designer_name in designers %}
<option value="{{ designer_id }}" {% if designer_id == value %}selected{% endif %}>{{ designer_name }}</option>
{% endfor %}
</select>
{% elif field == 'manufacturer' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<option value="">None</option>
{% for manufacturer_id, manufacturer_name in manufacturers %}
<option value="{{ manufacturer_id }}" {% if manufacturer_id == value %}selected{% endif %}>{{ manufacturer_name }}</option>
{% endfor %}
</select>
{% elif field == 'ride_model' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<option value="">None</option>
{% for model_id, model_name in ride_models %}
<option value="{{ model_id }}" {% if model_id == value %}selected{% endif %}>{{ model_name }}</option>
{% endfor %}
</select>
{% elif field == 'park_area' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<option value="">None</option>
{% for area_id, area_name in park_areas %}
<option value="{{ area_id }}" {% if area_id == value %}selected{% endif %}>{{ area_name }}</option>
{% endfor %}
</select>
{% else %}
<input type="{% if field == 'opening_date' or field == 'closing_date' or field == 'status_since' %}date{% else %}text{% endif %}"
name="{{ field }}"
value="{{ value }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
{% endif %}
</div>
{% endif %}
{% endfor %}
<div class="p-4 border rounded-lg bg-blue-50 dark:bg-blue-900/30 border-blue-200/50 dark:border-blue-700/50">
<label class="block mb-2 text-sm font-medium text-blue-900 dark:text-blue-300">
Notes (required):
</label>
<textarea name="notes"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg resize-none dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50"
rows="3"
required
placeholder="Explain why you're editing this submission"></textarea>
</div>
<div class="flex justify-end gap-3">
<button type="button"
class="px-4 py-2 font-medium text-gray-700 transition-all duration-200 bg-gray-100 rounded-lg hover:bg-gray-200 hover:text-gray-900 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
@click="showEditForm = false">
Cancel
</button>
<button type="submit"
class="px-4 py-2 font-medium text-white transition-all duration-200 bg-blue-600 rounded-lg hover:bg-blue-500 dark:bg-blue-700 dark:hover:bg-blue-600">
Save Changes
</button>
</div>
</form>
</div>

View File

@@ -1,44 +0,0 @@
<div class="mb-6 filters">
<form class="flex flex-wrap items-end gap-4"
hx-get="{% url 'moderation:submission_list' %}"
hx-target="#submissions-content"
hx-trigger="change"
hx-push-url="true">
<div class="flex-1 min-w-[200px]">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Status
</label>
<select name="status" class="w-full form-select">
<option value="">All Statuses</option>
<option value="PENDING" {% if request.GET.status == 'PENDING' %}selected{% endif %}>Pending</option>
<option value="APPROVED" {% if request.GET.status == 'APPROVED' %}selected{% endif %}>Approved</option>
<option value="REJECTED" {% if request.GET.status == 'REJECTED' %}selected{% endif %}>Rejected</option>
<option value="ESCALATED" {% if request.GET.status == 'ESCALATED' %}selected{% endif %}>Escalated</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Type
</label>
<select name="type" class="w-full form-select">
<option value="">All Types</option>
<option value="CREATE" {% if request.GET.type == 'CREATE' %}selected{% endif %}>New Submissions</option>
<option value="UPDATE" {% if request.GET.type == 'UPDATE' %}selected{% endif %}>Edit Submissions</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Content Type
</label>
<select name="content_type" class="w-full form-select">
<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>

View File

@@ -1,129 +0,0 @@
{% comment %}
This template contains the Alpine.js store for managing filter state in the moderation dashboard
{% endcomment %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('filters', {
active: [],
init() {
this.updateActiveFilters();
// Listen for filter changes
window.addEventListener('filter-changed', () => {
this.updateActiveFilters();
});
},
updateActiveFilters() {
const urlParams = new URLSearchParams(window.location.search);
this.active = [];
// Submission Type
if (urlParams.has('submission_type')) {
this.active.push({
name: 'submission_type',
label: 'Submission',
value: this.getSubmissionTypeLabel(urlParams.get('submission_type'))
});
}
// Type
if (urlParams.has('type')) {
this.active.push({
name: 'type',
label: 'Type',
value: this.getTypeLabel(urlParams.get('type'))
});
}
// Content Type
if (urlParams.has('content_type')) {
this.active.push({
name: 'content_type',
label: 'Content',
value: this.getContentTypeLabel(urlParams.get('content_type'))
});
}
},
getSubmissionTypeLabel(value) {
const labels = {
'text': 'Text',
'photo': 'Photo'
};
return labels[value] || value;
},
getTypeLabel(value) {
const labels = {
'CREATE': 'New',
'EDIT': 'Edit'
};
return labels[value] || value;
},
getContentTypeLabel(value) {
const labels = {
'park': 'Parks',
'ride': 'Rides',
'company': 'Companies'
};
return labels[value] || value;
},
get hasActiveFilters() {
return this.active.length > 0;
},
clear() {
const form = document.querySelector('form[hx-get]');
if (form) {
form.querySelectorAll('select').forEach(select => {
select.value = '';
});
form.dispatchEvent(new Event('change'));
}
},
// Accessibility Helpers
announceFilterChange() {
const message = this.hasActiveFilters
? `Applied filters: ${this.active.map(f => f.label + ': ' + f.value).join(', ')}`
: 'All filters cleared';
const announcement = document.createElement('div');
announcement.setAttribute('role', 'status');
announcement.setAttribute('aria-live', 'polite');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => announcement.remove(), 1000);
}
});
});
// Watch for filter changes and update URL params
document.addEventListener('filter-changed', (e) => {
const form = e.target.closest('form');
if (!form) return;
const formData = new FormData(form);
const params = new URLSearchParams();
for (let [key, value] of formData.entries()) {
if (value) {
params.append(key, value);
}
}
// Update URL without page reload
const newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
window.history.pushState({}, '', newUrl);
// Announce changes for screen readers
Alpine.store('filters').announceFilterChange();
});
</script>

View File

@@ -1,66 +0,0 @@
{% load static %}
<div class="animate-pulse">
<!-- Filter Bar Skeleton -->
<div class="flex items-center justify-between p-4 mb-6 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="flex items-center space-x-4">
{% for i in "1234" %}
<div class="w-24 h-10 bg-gray-200 rounded-lg dark:bg-gray-700"></div>
{% endfor %}
</div>
<div class="w-24 h-10 bg-gray-200 rounded-lg dark:bg-gray-700"></div>
</div>
<!-- Filter Form Skeleton -->
<div class="p-6 mb-6 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="flex flex-wrap items-end gap-4">
{% for i in "123" %}
<div class="flex-1 min-w-[200px] space-y-2">
<div class="w-24 h-4 bg-gray-200 rounded dark:bg-gray-700"></div>
<div class="w-full h-10 bg-gray-200 rounded-lg dark:bg-gray-700"></div>
</div>
{% endfor %}
</div>
</div>
<!-- Submission List Skeleton -->
{% for i in "123" %}
<div class="p-6 mb-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<!-- Left Column -->
<div class="space-y-4 md:col-span-1">
<div class="flex items-center gap-3">
<div class="w-24 h-6 bg-gray-200 rounded-lg dark:bg-gray-700"></div>
</div>
<div class="space-y-3">
{% for i in "1234" %}
<div class="flex items-center gap-2">
<div class="w-5 h-5 bg-gray-200 rounded dark:bg-gray-700"></div>
<div class="w-32 h-4 bg-gray-200 rounded dark:bg-gray-700"></div>
</div>
{% endfor %}
</div>
</div>
<!-- Right Column -->
<div class="md:col-span-2">
{% for i in "12" %}
<div class="p-4 mb-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<div class="w-24 h-4 mb-2 bg-gray-200 rounded dark:bg-gray-700"></div>
<div class="w-full h-4 bg-gray-200 rounded dark:bg-gray-700"></div>
</div>
{% endfor %}
<div class="grid grid-cols-1 gap-3 mt-4 md:grid-cols-2">
{% for i in "1234" %}
<div class="p-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<div class="w-24 h-4 mb-2 bg-gray-200 rounded dark:bg-gray-700"></div>
<div class="w-full h-4 bg-gray-200 rounded dark:bg-gray-700"></div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>

View File

@@ -1,48 +0,0 @@
{% load moderation_tags %}
<div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3>
<!-- Map Container -->
<div class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"
id="viewMap-{{ submission.id }}"
x-init="setTimeout(() => {
const map = L.map('viewMap-{{ submission.id }}');
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
{% if submission.changes.latitude and submission.changes.longitude %}
const lat = {{ submission.changes.latitude }};
const lng = {{ submission.changes.longitude }};
map.setView([lat, lng], 13);
L.marker([lat, lng]).addTo(map);
{% else %}
map.setView([0, 0], 2);
{% endif %}
}, 100)"></div>
<!-- Address Display -->
<div class="mt-4 space-y-1">
{% if submission.changes.street_address %}
<div class="flex items-center text-gray-600 dark:text-gray-400">
<i class="w-5 mr-2 fas fa-map-marker-alt"></i>
{{ submission.changes.street_address }}
</div>
{% endif %}
<div class="flex items-center text-gray-600 dark:text-gray-400">
<i class="w-5 mr-2 fas fa-city"></i>
{% if submission.changes.city %}{{ submission.changes.city }}{% endif %}
{% if submission.changes.state %}, {{ submission.changes.state }}{% endif %}
{% if submission.changes.postal_code %} {{ submission.changes.postal_code }}{% endif %}
</div>
{% if submission.changes.country %}
<div class="flex items-center text-gray-600 dark:text-gray-400">
<i class="w-5 mr-2 fas fa-globe"></i>
{{ submission.changes.country }}
</div>
{% endif %}
</div>
</div>

View File

@@ -1,414 +0,0 @@
{% load static %}
<style>
/* Ensure map container and its elements stay below other UI elements */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
z-index: 1 !important;
}
.leaflet-control {
z-index: 2 !important;
}
</style>
<div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3>
<div class="location-widget" id="locationWidget-{{ submission.id }}">
{# Search Form #}
<div class="relative mb-4">
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Search Location
</label>
<input type="text"
id="locationSearch-{{ submission.id }}"
class="relative w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
placeholder="Search for a location..."
autocomplete="off"
style="z-index: 10;">
<div id="searchResults-{{ submission.id }}"
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
class="hidden w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600">
</div>
</div>
{# Map Container #}
<div class="relative mb-4" style="z-index: 1;">
<div id="locationMap-{{ submission.id }}"
class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
</div>
{# Location Form Fields #}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Street Address
</label>
<input type="text"
name="street_address"
id="streetAddress-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.street_address }}">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
City
</label>
<input type="text"
name="city"
id="city-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.city }}">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
State/Region
</label>
<input type="text"
name="state"
id="state-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.state }}">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Country
</label>
<input type="text"
name="country"
id="country-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.country }}">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Postal Code
</label>
<input type="text"
name="postal_code"
id="postalCode-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.postal_code }}">
</div>
</div>
{# Hidden Coordinate Fields #}
<div class="hidden">
<input type="hidden" name="latitude" id="latitude-{{ submission.id }}" value="{{ submission.changes.latitude }}">
<input type="hidden" name="longitude" id="longitude-{{ submission.id }}" value="{{ submission.changes.longitude }}">
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let maps = {};
let markers = {};
const searchInput = document.getElementById('locationSearch-{{ submission.id }}');
const searchResults = document.getElementById('searchResults-{{ submission.id }}');
let searchTimeout;
// Initialize form fields with existing values
const fields = {
city: '{{ submission.changes.city|default:"" }}',
state: '{{ submission.changes.state|default:"" }}',
country: '{{ submission.changes.country|default:"" }}',
postal_code: '{{ submission.changes.postal_code|default:"" }}',
street_address: '{{ submission.changes.street_address|default:"" }}',
latitude: '{{ submission.changes.latitude|default:"" }}',
longitude: '{{ submission.changes.longitude|default:"" }}'
};
Object.entries(fields).forEach(([field, value]) => {
const element = document.getElementById(`${field}-{{ submission.id }}`);
if (element) {
element.value = value;
}
});
// Set initial search input value if location exists
if (fields.street_address || fields.city) {
const parts = [
fields.street_address,
fields.city,
fields.state,
fields.country
].filter(Boolean);
searchInput.value = parts.join(', ');
}
function normalizeCoordinate(value, maxDigits, decimalPlaces) {
if (!value) return null;
try {
const rounded = Number(value).toFixed(decimalPlaces);
const strValue = rounded.replace('.', '').replace('-', '');
const strippedValue = strValue.replace(/0+$/, '');
if (strippedValue.length > maxDigits) {
return Number(Number(value).toFixed(decimalPlaces - 1));
}
return rounded;
} catch (error) {
console.error('Coordinate normalization failed:', error);
return null;
}
}
function validateCoordinates(lat, lng) {
const normalizedLat = normalizeCoordinate(lat, 9, 6);
const normalizedLng = normalizeCoordinate(lng, 10, 6);
if (normalizedLat === null || normalizedLng === null) {
throw new Error('Invalid coordinate format');
}
const parsedLat = parseFloat(normalizedLat);
const parsedLng = parseFloat(normalizedLng);
if (parsedLat < -90 || parsedLat > 90) {
throw new Error('Latitude must be between -90 and 90 degrees.');
}
if (parsedLng < -180 || parsedLng > 180) {
throw new Error('Longitude must be between -180 and 180 degrees.');
}
return { lat: normalizedLat, lng: normalizedLng };
}
function initMap() {
const submissionId = '{{ submission.id }}';
const mapId = `locationMap-${submissionId}`;
const mapContainer = document.getElementById(mapId);
if (!mapContainer) {
console.error(`Map container ${mapId} not found`);
return;
}
// If map already exists, remove it
if (maps[submissionId]) {
maps[submissionId].remove();
delete maps[submissionId];
delete markers[submissionId];
}
// Create new map
maps[submissionId] = L.map(mapId);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(maps[submissionId]);
// Initialize with existing coordinates if available
const initialLat = fields.latitude;
const initialLng = fields.longitude;
if (initialLat && initialLng) {
try {
const normalized = validateCoordinates(initialLat, initialLng);
maps[submissionId].setView([normalized.lat, normalized.lng], 13);
addMarker(normalized.lat, normalized.lng);
} catch (error) {
console.error('Invalid initial coordinates:', error);
maps[submissionId].setView([0, 0], 2);
}
} else {
maps[submissionId].setView([0, 0], 2);
}
// Handle map clicks
maps[submissionId].on('click', async function(e) {
try {
const normalized = validateCoordinates(e.latlng.lat, e.latlng.lng);
const response = await fetch(`/parks/search/reverse-geocode/?lat=${normalized.lat}&lon=${normalized.lng}`);
if (!response.ok) {
throw new Error('Geocoding request failed');
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
updateLocation(normalized.lat, normalized.lng, data);
} catch (error) {
console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.');
}
});
}
function addMarker(lat, lng) {
const submissionId = '{{ submission.id }}';
if (markers[submissionId]) {
markers[submissionId].remove();
}
markers[submissionId] = L.marker([lat, lng]).addTo(maps[submissionId]);
maps[submissionId].setView([lat, lng], 13);
}
function updateLocation(lat, lng, data) {
try {
const normalized = validateCoordinates(lat, lng);
const submissionId = '{{ submission.id }}';
// Update coordinates
document.getElementById(`latitude-${submissionId}`).value = normalized.lat;
document.getElementById(`longitude-${submissionId}`).value = normalized.lng;
// Update marker
addMarker(normalized.lat, normalized.lng);
// Update form fields with English names where available
const address = data.address || {};
document.getElementById(`streetAddress-${submissionId}`).value =
`${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
document.getElementById(`city-${submissionId}`).value =
address.city || address.town || address.village || '';
document.getElementById(`state-${submissionId}`).value =
address.state || address.region || '';
document.getElementById(`country-${submissionId}`).value = address.country || '';
document.getElementById(`postalCode-${submissionId}`).value = address.postcode || '';
// Update search input
const locationString-3 = [
document.getElementById(`streetAddress-${submissionId}`).value,
document.getElementById(`city-${submissionId}`).value,
document.getElementById(`state-${submissionId}`).value,
document.getElementById(`country-${submissionId}`).value
].filter(Boolean).join(', ');
searchInput.value = locationString;
} catch (error) {
console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.');
}
}
function selectLocation(result) {
if (!result) return;
try {
const lat = parseFloat(result.lat);
const lon = parseFloat(result.lon);
if (isNaN(lat) || isNaN(lon)) {
throw new Error('Invalid coordinates in search result');
}
const normalized = validateCoordinates(lat, lon);
// Create a normalized address object
const address = {
name: result.display_name || result.name || '',
address: {
house_number: result.address ? result.address.house_number : '',
road: result.address ? (result.address.road || result.address.street) : '',
city: result.address ? (result.address.city || result.address.town || result.address.village) : '',
state: result.address ? (result.address.state || result.address.region) : '',
country: result.address ? result.address.country : '',
postcode: result.address ? result.address.postcode : ''
}
};
updateLocation(normalized.lat, normalized.lng, address);
searchResults.classList.add('hidden');
searchInput.value = address.name;
} catch (error) {
console.error('Location selection failed:', error);
alert(error.message || 'Failed to select location. Please try again.');
}
}
// Handle location search
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (!query) {
searchResults.classList.add('hidden');
return;
}
searchTimeout = setTimeout(async function() {
try {
const response = await fetch(`/parks/search/location/?q=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error('Search request failed');
}
const data = await response.json();
if (data.results && data.results.length > 0) {
const resultsHtml = data.results.map((result, index) => `
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
data-result-index="${index}">
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
${(result.address && result.address.city) ? result.address.city + ', ' : ''}${(result.address && result.address.country) || ''}
</div>
</div>
`).join('');
searchResults.innerHTML = resultsHtml;
searchResults.classList.remove('hidden');
// Store results data
searchResults.dataset.results = JSON.stringify(data.results);
// Add click handlers
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
el.addEventListener('click', function() {
const results = JSON.parse(searchResults.dataset.results);
const result = results[this.dataset.resultIndex];
selectLocation(result);
});
});
} else {
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
searchResults.classList.remove('hidden');
}
} catch (error) {
console.error('Search failed:', error);
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
searchResults.classList.remove('hidden');
}
}, 300);
});
// Hide search results when clicking outside
document.addEventListener('click', function(e) {
if (!searchResults.contains(e.target) && e.target !== searchInput) {
searchResults.classList.add('hidden');
}
});
// Initialize map when the element becomes visible
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
if (mapContainer && window.getComputedStyle(mapContainer).display !== 'none') {
initMap();
observer.disconnect();
}
}
});
});
const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
if (mapContainer) {
observer.observe(mapContainer.parentElement.parentElement, { attributes: true });
// Also initialize immediately if the container is already visible
if (window.getComputedStyle(mapContainer).display !== 'none') {
initMap();
}
}
});
</script>

View File

@@ -1,67 +0,0 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
{% if manufacturers %}
{% for manufacturer in manufacturers %}
<button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectManufacturerForSubmission('{{ manufacturer.id }}', '{{ manufacturer.name|escapejs }}', '{{ submission_id }}')">
{{ manufacturer.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No manufacturers found
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectManufacturerForSubmission(id, name, submissionId) {
// Debug logging
console.log('Selecting manufacturer:', {id, name, submissionId});
// Find elements
const manufacturerInput = document.querySelector(`#manufacturer-input-${submissionId}`);
const searchInput = document.querySelector(`#manufacturer-search-${submissionId}`);
const resultsDiv = document.querySelector(`#manufacturer-search-results-${submissionId}`);
// Debug logging
console.log('Found elements:', {
manufacturerInput: manufacturerInput?.id,
searchInput: searchInput?.id,
resultsDiv: resultsDiv?.id
});
// Update hidden input
if (manufacturerInput) {
manufacturerInput.value = id;
console.log('Updated manufacturer input value:', manufacturerInput.value);
}
// Update search input
if (searchInput) {
searchInput.value = name;
console.log('Updated search input value:', searchInput.value);
}
// Clear results
if (resultsDiv) {
resultsDiv.innerHTML = '';
console.log('Cleared results div');
}
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="manufacturer-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#manufacturer-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
}
});
});
</script>

View File

@@ -1,43 +0,0 @@
{% load static %}
<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,73 +0,0 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
{% if parks %}
{% for park in parks %}
<button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectParkForSubmission('{{ park.id }}', '{{ park.name|escapejs }}', '{{ submission_id }}')">
{{ park.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No parks found
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectParkForSubmission(id, name, submissionId) {
// Debug logging
console.log('Selecting park:', {id, name, submissionId});
// Find elements
const parkInput = document.querySelector(`#park-input-${submissionId}`);
const searchInput = document.querySelector(`#park-search-${submissionId}`);
const resultsDiv = document.querySelector(`#park-search-results-${submissionId}`);
// Debug logging
console.log('Found elements:', {
parkInput: parkInput?.id,
searchInput: searchInput?.id,
resultsDiv: resultsDiv?.id
});
// Update hidden input
if (parkInput) {
parkInput.value = id;
console.log('Updated park input value:', parkInput.value);
}
// Update search input
if (searchInput) {
searchInput.value = name;
console.log('Updated search input value:', searchInput.value);
}
// Clear results
if (resultsDiv) {
resultsDiv.innerHTML = '';
console.log('Cleared results div');
}
// Trigger park areas update
if (parkInput) {
htmx.trigger(parkInput, 'change');
console.log('Triggered change event');
}
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="park-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#park-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
}
});
});
</script>

View File

@@ -1,110 +0,0 @@
<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 == 'PENDING' %}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 == 'PENDING' %}clock
{% elif submission.status == 'APPROVED' %}check
{% elif submission.status == 'REJECTED' %}times
{% elif submission.status == 'ESCALATED' %}exclamation{% endif %}"></i>
{{ submission.get_status_display }}
</span>
Photo for {{ submission.content_object }}
</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>
{% if submission.date_taken %}
<span class="mx-2"></span>
<span class="inline-flex items-center">
<i class="mr-1 fas fa-calendar"></i>
Taken: {{ submission.date_taken|date:"M d, Y" }}
</span>
{% endif %}
</div>
</div>
</div>
<div class="mt-4 overflow-hidden bg-gray-100 rounded-lg aspect-w-16 aspect-h-9 dark:bg-gray-800">
<img src="{{ submission.photo.url }}"
alt="{{ submission.caption|default:'Submitted photo' }}"
class="object-contain w-full h-full">
</div>
{% if submission.caption %}
<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">Caption:</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">{{ submission.caption }}</div>
</div>
{% endif %}
{% if submission.notes %}
<div class="p-4 mt-4 border rounded-lg bg-blue-50 dark:bg-blue-900/30 border-blue-200/50 dark:border-blue-700/50">
<div class="text-sm font-medium text-blue-900 dark:text-blue-300">Review Notes:</div>
<div class="mt-1.5 text-blue-800 dark:text-blue-200">{{ submission.notes }}</div>
</div>
{% endif %}
{% if submission.status == 'PENDING' 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>
{% if submission.status != 'ESCALATED' or user.role in 'ADMIN,SUPERUSER' %}
<button class="btn-approve"
hx-post="{% url 'moderation:approve_photo' submission.id %}"
hx-target="#submission-{{ submission.id }}"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to approve this photo?"
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-check"></i>
Approve
</button>
<button class="btn-reject"
hx-post="{% url 'moderation:reject_photo' submission.id %}"
hx-target="#submission-{{ submission.id }}"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to reject this photo?"
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_photo' submission.id %}"
hx-target="#submission-{{ submission.id }}"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to escalate this photo?"
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-arrow-up"></i>
Escalate
</button>
{% endif %}
</div>
</div>
{% endif %}
</div>

View File

@@ -1,23 +0,0 @@
{% load static %}
<div class="space-y-6">
<div class="p-6 bg-white border rounded-lg shadow-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Photo Submissions</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>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for submission in submissions %}
{% include "moderation/partials/photo_submission.html" %}
{% empty %}
<div class="py-8 text-center col-span-full">
<i class="mb-3 text-4xl text-gray-400 fas fa-camera"></i>
<p class="text-gray-600 dark:text-gray-400">No photo submissions found</p>
</div>
{% endfor %}
</div>
</div>
</div>

View File

@@ -1,67 +0,0 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
{% if ride_models %}
{% for model in ride_models %}
<button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectRideModelForSubmission('{{ model.id }}', '{{ model.name|escapejs }}', '{{ submission_id }}')">
{{ model.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No ride models found
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectRideModelForSubmission(id, name, submissionId) {
// Debug logging
console.log('Selecting ride model:', {id, name, submissionId});
// Find elements
const modelInput = document.querySelector(`#ride-model-input-${submissionId}`);
const searchInput = document.querySelector(`#ride-model-search-${submissionId}`);
const resultsDiv = document.querySelector(`#ride-model-search-results-${submissionId}`);
// Debug logging
console.log('Found elements:', {
modelInput: modelInput?.id,
searchInput: searchInput?.id,
resultsDiv: resultsDiv?.id
});
// Update hidden input
if (modelInput) {
modelInput.value = id;
console.log('Updated ride model input value:', modelInput.value);
}
// Update search input
if (searchInput) {
searchInput.value = name;
console.log('Updated search input value:', searchInput.value);
}
// Clear results
if (resultsDiv) {
resultsDiv.innerHTML = '';
console.log('Cleared results div');
}
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="ride-model-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#ride-model-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
}
});
});
</script>

View File

@@ -1,498 +0,0 @@
{% load moderation_tags %}
{% load static %}
{% block extra_head %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
{% endblock %}
{% for submission in submissions %}
<div class="p-6 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50"
id="submission-{{ submission.id }}"
x-data="{
showSuccess: false,
isEditing: false,
status: '{{ submission.changes.status|default:"" }}',
category: '{{ submission.changes.category|default:"" }}',
showCoasterFields: {% if submission.changes.category == 'RC' %}true{% else %}false{% endif %},
init() {
this.$watch('category', value => {
this.showCoasterFields = value === 'RC';
});
}
}"
@htmx:afterOnLoad="showSuccess = true; setTimeout(() => showSuccess = false, 3000)">
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<!-- Left Column: Header & Status -->
<div class="md:col-span-1">
<div class="submission-header">
<h3 class="flex items-center gap-3 text-lg font-semibold text-gray-900 dark:text-gray-300">
<span class="status-badge
{% if submission.status == 'PENDING' %}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 == 'PENDING' %}clock
{% elif submission.status == 'APPROVED' %}check
{% elif submission.status == 'REJECTED' %}times
{% elif submission.status == 'ESCALATED' %}exclamation{% endif %}"></i>
{{ submission.get_status_display }}
</span>
</h3>
<div class="mt-3 text-gray-600 dark:text-gray-400">
<div class="flex items-center mb-2">
<i class="w-5 mr-2 fas fa-file-alt"></i>
{{ submission.get_content_type_display }} -
{% if submission.submission_type == 'CREATE' %}New{% else %}Edit{% endif %}
</div>
<div class="flex items-center mb-2">
<i class="w-5 mr-2 fas fa-user"></i>
{{ submission.user.username }}
</div>
<div class="flex items-center mb-2">
<i class="w-5 mr-2 fas fa-clock"></i>
{{ submission.created_at|date:"M d, Y H:i" }}
</div>
{% if submission.handled_by %}
<div class="flex items-center">
<i class="w-5 mr-2 fas fa-user-shield"></i>
{{ submission.handled_by.username }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Middle Column: Content Details -->
<div class="md:col-span-2">
{% if submission.content_object %}
<div class="p-4 mb-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<div class="text-sm font-medium text-gray-900 dark:text-gray-300">Current Object:</div>
<div class="mt-1.5 text-gray-600 dark:text-gray-400">
{{ submission.content_object }}
{% if submission.content_type.model == 'park' or submission.content_type.model == 'company' or submission.content_type.model == 'designer' %}
<div class="mt-1 text-sm">
{{ submission.content_object.address }}
</div>
{% endif %}
</div>
</div>
{% endif %}
{% if submission.reason %}
<div class="p-4 mb-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<div class="text-sm font-medium text-gray-900 dark:text-gray-300">Reason:</div>
<div class="mt-1.5 text-gray-600 dark:text-gray-400">{{ submission.reason }}</div>
</div>
{% endif %}
{% if submission.source %}
<div class="p-4 mb-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<div class="text-sm font-medium text-gray-900 dark:text-gray-300">Source:</div>
<div class="mt-1.5">
<a href="{{ submission.source }}"
target="_blank"
class="inline-flex items-center text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
<span>{{ submission.source }}</span>
<i class="ml-1.5 text-xs fas fa-external-link-alt"></i>
</a>
</div>
</div>
{% endif %}
<!-- View Mode -->
<div x-show="!isEditing">
<!-- Location Map (View Mode) -->
{% if submission.content_type.model == 'park' %}
<div class="mb-4">
{% include "moderation/partials/location_map.html" with submission=submission %}
</div>
{% endif %}
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{% for field, value in submission.changes.items %}
{% if field != 'model_name' and field != 'latitude' and field != 'longitude' and field != 'street_address' and field != 'city' and field != 'state' and field != 'postal_code' and field != 'country' and field != 'location' %}
<div class="p-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<div class="text-sm font-medium text-gray-900 dark:text-gray-300">
{{ field|title }}:
</div>
<div class="mt-1.5 text-gray-600 dark:text-gray-400">
{% if field == 'opening_date' or field == 'closing_date' or field == 'status_since' %}
{{ value|date:"Y-m-d" }}
{% elif field == 'size_acres' %}
{{ value }} acres
{% elif field == 'website' %}
<a href="{{ value }}" target="_blank" class="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
{{ value }}
</a>
{% elif field == 'park' %}
{% with park_name=value|get_object_name:'parks.Park' %}
{{ park_name }}
{% endwith %}
{% elif field == 'designer' %}
{% with designer_name=value|get_object_name:'designers.Designer' %}
{{ designer_name|default:'None' }}
{% endwith %}
{% elif field == 'manufacturer' %}
{% with manufacturer_name=value|get_object_name:'companies.Manufacturer' %}
{{ manufacturer_name|default:'None' }}
{% endwith %}
{% elif field == 'ride_model' %}
{% with model_name=value|get_object_name:'rides.RideModel' %}
{{ model_name|default:'None' }}
{% endwith %}
{% elif field == 'park_area' %}
{% with park_id=submission.changes.park %}
{{ value|get_park_area_name:park_id|default:'None' }}
{% endwith %}
{% elif field == 'category' %}
{{ value|get_category_display }}
{% elif field == 'stats' %}
<div class="space-y-2">
{% if value.height_ft %}<div>Height: {{ value.height_ft }} ft</div>{% endif %}
{% if value.length_ft %}<div>Length: {{ value.length_ft }} ft</div>{% endif %}
{% if value.speed_mph %}<div>Speed: {{ value.speed_mph }} mph</div>{% endif %}
{% if value.inversions %}<div>Inversions: {{ value.inversions }}</div>{% endif %}
{% if value.launch_type %}<div>Launch Type: {{ value.launch_type }}</div>{% endif %}
{% if value.track_material %}<div>Track Material: {{ value.track_material }}</div>{% endif %}
{% if value.roller_coaster_type %}<div>Type: {{ value.roller_coaster_type }}</div>{% endif %}
{% if value.trains_count %}<div>Number of Trains: {{ value.trains_count }}</div>{% endif %}
{% if value.cars_per_train %}<div>Cars per Train: {{ value.cars_per_train }}</div>{% endif %}
{% if value.seats_per_car %}<div>Seats per Car: {{ value.seats_per_car }}</div>{% endif %}
</div>
{% else %}
{{ value }}
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<!-- Edit Mode -->
<form x-show="isEditing"
x-cloak
hx-post="{% url 'moderation:edit_submission' submission.id %}"
hx-target="#submission-{{ submission.id }}"
class="grid grid-cols-1 gap-3 md:grid-cols-2">
<!-- Location Widget for Parks -->
{% if submission.content_type.model == 'park' %}
<div class="col-span-2">
{% include "moderation/partials/location_widget.html" with submission=submission %}
</div>
{% endif %}
{% for field, value in submission.changes.items %}
{% if field != 'model_name' and field != 'stats' and field != 'latitude' and field != 'longitude' and field != 'street_address' and field != 'city' and field != 'state' and field != 'postal_code' and field != 'country' and field != 'location' %}
<div class="p-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50"
{% if field == 'post_closing_status' %}x-show="status === 'CLOSING'"{% endif %}
{% if field == 'closing_date' %}x-show="['CLOSING', 'SBNO', 'CLOSED_PERM', 'DEMOLISHED', 'RELOCATED'].includes(status)"{% endif %}>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
{{ field|title }}:
</label>
{% if field == 'category' %}
<select name="{{ field }}"
x-model="category"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500">
<option value="">Select ride type</option>
<option value="RC" {% if value == 'RC' %}selected{% endif %}>Roller Coaster</option>
<option value="DR" {% if value == 'DR' %}selected{% endif %}>Dark Ride</option>
<option value="FR" {% if value == 'FR' %}selected{% endif %}>Flat Ride</option>
<option value="WR" {% if value == 'WR' %}selected{% endif %}>Water Ride</option>
<option value="TR" {% if value == 'TR' %}selected{% endif %}>Transport</option>
<option value="OT" {% if value == 'OT' %}selected{% endif %}>Other</option>
</select>
{% elif field == 'status' %}
<select name="{{ field }}"
x-model="status"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500">
<option value="OPERATING" {% if value == 'OPERATING' %}selected{% endif %}>Operating</option>
<option value="SBNO" {% if value == 'SBNO' %}selected{% endif %}>Standing But Not Operating</option>
<option value="CLOSING" {% if value == 'CLOSING' %}selected{% endif %}>Closing</option>
<option value="CLOSED_PERM" {% if value == 'CLOSED_PERM' %}selected{% endif %}>Permanently Closed</option>
<option value="UNDER_CONSTRUCTION" {% if value == 'UNDER_CONSTRUCTION' %}selected{% endif %}>Under Construction</option>
<option value="DEMOLISHED" {% if value == 'DEMOLISHED' %}selected{% endif %}>Demolished</option>
<option value="RELOCATED" {% if value == 'RELOCATED' %}selected{% endif %}>Relocated</option>
</select>
{% elif field == 'post_closing_status' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500">
<option value="SBNO" {% if value == 'SBNO' %}selected{% endif %}>Standing But Not Operating</option>
<option value="CLOSED_PERM" {% if value == 'CLOSED_PERM' %}selected{% endif %}>Permanently Closed</option>
</select>
{% elif field == 'opening_date' or field == 'closing_date' or field == 'status_since' %}
<input type="date"
name="{{ field }}"
value="{{ value|date:'Y-m-d' }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
{% if field == 'closing_date' %}
:required="status === 'CLOSING'"
{% endif %}>
{% else %}
{% if field == 'park' %}
<div class="relative space-y-2">
<input type="text"
id="park-search-{{ submission.id }}"
placeholder="Search for a park..."
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
hx-get="{% url 'moderation:search_parks' %}"
hx-trigger="click, input delay:200ms"
hx-target="#park-search-results-{{ submission.id }}"
hx-vals='{"submission_id": "{{ submission.id }}"}'
name="q"
autocomplete="off"
{% with park_name=value|get_object_name:'parks.Park' %}
value="{{ park_name }}"
{% endwith %}>
<div id="park-search-results-{{ submission.id }}" class="absolute z-50 w-full"></div>
<input type="hidden"
id="park-input-{{ submission.id }}"
name="{{ field }}"
value="{{ value }}"
hx-trigger="change"
hx-get="/parks/areas/"
hx-target="#park-area-select-{{ submission.id }}">
</div>
{% elif field == 'park_area' %}
<select name="{{ field }}"
id="park-area-select-{{ submission.id }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500">
<option value="">Select area</option>
{% with park_id=submission.changes.park %}
{% with areas=park_areas_by_park|get_item:park_id %}
{% for area_id, area_name in areas %}
<option value="{{ area_id }}" {% if value == area_id %}selected{% endif %}>{{ area_name }}</option>
{% endfor %}
{% endwith %}
{% endwith %}
</select>
{% elif field == 'manufacturer' %}
<div class="relative space-y-2">
<input type="text"
id="manufacturer-search-{{ submission.id }}"
placeholder="Search for a manufacturer..."
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
hx-get="{% url 'moderation:search_manufacturers' %}"
hx-trigger="click, input delay:200ms"
hx-target="#manufacturer-search-results-{{ submission.id }}"
hx-vals='{"submission_id": "{{ submission.id }}"}'
name="q"
autocomplete="off"
{% with manufacturer_name=value|get_object_name:'companies.Manufacturer' %}
value="{{ manufacturer_name }}"
{% endwith %}>
<div id="manufacturer-search-results-{{ submission.id }}" class="absolute z-50 w-full"></div>
<input type="hidden"
id="manufacturer-input-{{ submission.id }}"
name="{{ field }}"
value="{{ value }}">
</div>
{% elif field == 'designer' %}
<div class="relative space-y-2">
<input type="text"
id="designer-search-{{ submission.id }}"
placeholder="Search for a designer..."
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
hx-get="{% url 'moderation:search_designers' %}"
hx-trigger="click, input delay:200ms"
hx-target="#designer-search-results-{{ submission.id }}"
hx-vals='{"submission_id": "{{ submission.id }}"}'
name="q"
autocomplete="off"
{% with designer_name=value|get_object_name:'designers.Designer' %}
value="{{ designer_name }}"
{% endwith %}>
<div id="designer-search-results-{{ submission.id }}" class="absolute z-50 w-full"></div>
<input type="hidden"
id="designer-input-{{ submission.id }}"
name="{{ field }}"
value="{{ value }}">
</div>
{% elif field == 'ride_model' %}
<div class="relative space-y-2">
<input type="text"
id="ride-model-search-{{ submission.id }}"
placeholder="Search for a ride model..."
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
hx-get="{% url 'moderation:search_ride_models' %}"
hx-trigger="click, input delay:200ms"
hx-target="#ride-model-search-results-{{ submission.id }}"
hx-vals='{"submission_id": "{{ submission.id }}"}'
hx-include="[name='manufacturer']"
name="q"
autocomplete="off"
{% with model_name=value|get_object_name:'rides.RideModel' %}
value="{{ model_name }}"
{% endwith %}>
<div id="ride-model-search-results-{{ submission.id }}" class="absolute z-50 w-full"></div>
<input type="hidden"
id="ride-model-input-{{ submission.id }}"
name="{{ field }}"
value="{{ value }}">
</div>
{% elif field == 'description' %}
<textarea name="{{ field }}"
rows="4"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="General description and notable features">{{ value }}</textarea>
{% elif field == 'min_height_in' or field == 'max_height_in' %}
<input type="number"
name="{{ field }}"
value="{{ value }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Height in inches">
{% elif field == 'capacity_per_hour' %}
<input type="number"
name="{{ field }}"
value="{{ value }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Theoretical hourly ride capacity">
{% elif field == 'ride_duration_seconds' %}
<input type="number"
name="{{ field }}"
value="{{ value }}"
min="0"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
placeholder="Total duration of one ride cycle in seconds">
{% else %}
<input type="text"
name="{{ field }}"
value="{{ value }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500">
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
<!-- Coaster Fields -->
<div x-show="showCoasterFields"
x-cloak
class="col-span-2">
{% include 'moderation/partials/coaster_fields.html' with stats=submission.changes.stats %}
</div>
<div class="col-span-2 p-4 border rounded-lg bg-blue-50 dark:bg-blue-900/30 border-blue-200/50 dark:border-blue-700/50">
<label class="block mb-2 text-sm font-medium text-blue-900 dark:text-blue-300">
Notes (required):
</label>
<textarea name="notes"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg resize-none dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500"
rows="3"
required
placeholder="Explain why you're editing this submission"></textarea>
</div>
<div class="flex justify-end col-span-2 gap-3">
<button type="button"
class="px-4 py-2 font-medium text-gray-700 transition-all duration-200 bg-gray-100 rounded-lg hover:bg-gray-200 hover:text-gray-900 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
@click="isEditing = false">
Cancel
</button>
<button type="submit"
class="px-4 py-2 font-medium text-white transition-all duration-200 bg-blue-600 rounded-lg hover:bg-blue-500 dark:bg-blue-700 dark:hover:bg-blue-600">
Save Changes
</button>
</div>
</form>
<!-- Keep existing review notes section -->
{% if submission.notes %}
<div class="p-4 mt-4 border rounded-lg bg-blue-50 dark:bg-blue-900/30 border-blue-200/50 dark:border-blue-700/50">
<div class="text-sm font-medium text-blue-900 dark:text-blue-300">Review Notes:</div>
<div class="mt-1.5 text-blue-800 dark:text-blue-200">{{ submission.notes }}</div>
</div>
{% endif %}
{% if submission.status == 'PENDING' 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-900 bg-white border rounded-lg resize-none dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50 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="inline-flex items-center px-4 py-2.5 font-medium text-gray-700 transition-all duration-200 bg-gray-100 rounded-lg hover:bg-gray-200 hover:text-gray-900 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white shadow-xs hover:shadow-md"
@click="showNotes = !showNotes">
<i class="mr-2 fas fa-comment-alt"></i>
Add Notes
</button>
<button class="inline-flex items-center px-4 py-2.5 font-medium text-gray-700 transition-all duration-200 bg-gray-100 rounded-lg hover:bg-gray-200 hover:text-gray-900 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white shadow-xs hover:shadow-md"
@click="isEditing = !isEditing">
<i class="mr-2 fas fa-edit"></i>
Edit
</button>
{% if submission.status != 'ESCALATED' or user.role in 'ADMIN,SUPERUSER' %}
<button class="inline-flex items-center px-4 py-2.5 font-medium text-white transition-all duration-200 bg-green-600 rounded-lg hover:bg-green-500 dark:bg-green-700 dark:hover:bg-green-600 shadow-xs hover:shadow-md"
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="inline-flex items-center px-4 py-2.5 font-medium text-white transition-all duration-200 bg-red-600 rounded-lg hover:bg-red-500 dark:bg-red-700 dark:hover:bg-red-600 shadow-xs hover:shadow-md"
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="inline-flex items-center px-4 py-2.5 font-medium text-white transition-all duration-200 bg-yellow-600 rounded-lg hover:bg-yellow-500 dark:bg-yellow-700 dark:hover:bg-yellow-600 shadow-xs hover:shadow-md"
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>
</div>
</div>
{% empty %}
<div class="relative p-8 text-center bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="text-gray-600 dark: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 id="loading-indicator"
class="absolute inset-0 flex items-center justify-center rounded-lg htmx-indicator bg-white/80 dark:bg-gray-900/80">
<div class="flex items-center p-6 space-x-4">
<div class="w-8 h-8 border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
<span class="text-gray-900 dark:text-gray-300">Loading...</span>
</div>
</div>
</div>
{% endfor %}
{% block extra_js %}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
{% endblock %}

View File

@@ -1,7 +0,0 @@
{% extends "moderation/dashboard.html" %}
{% block moderation_content %}
<div id="submissions-content">
{% include "moderation/partials/photo_submission_content.html" %}
</div>
{% endblock %}