Revert "Add version control system functionality with branch management, history tracking, and merge operations"

This reverts commit f3d28817a5.
This commit is contained in:
pacnpal
2025-02-08 17:37:30 -05:00
parent 03f9df4bab
commit 71b73522ae
125 changed files with 617 additions and 15830 deletions

View File

@@ -146,196 +146,152 @@
{% block content %}
<div class="container max-w-6xl px-4 py-6 mx-auto">
<div id="dashboard-content"
class="relative transition-all duration-200"
hx-target="this"
hx-push-url="true"
hx-indicator="#loading-skeleton"
hx-swap="outerHTML">
<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 opacity-0"
id="loading-skeleton"
aria-hidden="true">
<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"
role="alert"
aria-live="assertive">
<div class="flex flex-col items-center justify-center h-full p-6 space-y-4 text-center"
x-data="{ errorMessage: 'There was a problem loading the content. Please try again.' }"
x-init="$watch('errorMessage', value => $dispatch('show-toast', { message: value, type: 'error' }))">
<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" aria-hidden="true"></i>
<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"
x-text="errorMessage"></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 disabled:opacity-50 disabled:cursor-not-allowed"
hx-get="{{ request.path }}"
hx-target="#dashboard-content"
hx-push-url="true"
hx-indicator="this"
@click="$el.disabled = true"
hx-on::after-request="$el.disabled = false">
<span class="htmx-indicator">
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>
Retrying...
</span>
<span class="htmx-settled">
<i class="mr-2 fas fa-sync-alt" aria-hidden="true"></i>
Retry
</span>
<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>
<!-- Toast Notifications -->
<div id="toast-container"
class="fixed bottom-4 right-4 z-50 space-y-2"
x-data="{
toasts: [],
add(message, type = 'success') {
const id = Date.now();
this.toasts.push({ id, message, type });
setTimeout(() => this.remove(id), 5000);
},
remove(id) {
this.toasts = this.toasts.filter(t => t.id !== id);
}
}"
@show-toast.window="add($event.detail.message, $event.detail.type)"
@htmx:responseError.window="add($event.detail.error || 'An error occurred', 'error')"
aria-live="polite"
aria-atomic="true">
<template x-for="toast in toasts" :key="toast.id">
<div class="flex items-center p-4 rounded-lg shadow-lg transform transition-all duration-300"
:class="{
'bg-green-600': toast.type === 'success',
'bg-red-600': toast.type === 'error',
'bg-yellow-600': toast.type === 'warning',
'bg-blue-600': toast.type === 'info'
}"
x-transition:enter="ease-out"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="ease-in"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<div class="flex-1 text-white">
<p class="font-medium" x-text="toast.message"></p>
</div>
<button @click="remove(toast.id)"
class="ml-4 text-white hover:text-white/80"
aria-label="Close notification">
<i class="fas fa-times"></i>
</button>
</div>
</template>
</div>
<!-- HTMX Event Handlers -->
<script>
document.body.addEventListener('htmx:beforeRequest', function(evt) {
const target = evt.detail.target;
if (target.hasAttribute('hx-disabled-elt')) {
const disabledElt = document.querySelector(target.getAttribute('hx-disabled-elt'));
if (disabledElt) {
disabledElt.disabled = true;
}
}
});
document.body.addEventListener('htmx:afterRequest', function(evt) {
const target = evt.detail.target;
if (target.hasAttribute('hx-disabled-elt')) {
const disabledElt = document.querySelector(target.getAttribute('hx-disabled-elt'));
if (disabledElt) {
disabledElt.disabled = false;
}
}
});
document.body.addEventListener('htmx:responseError', function(evt) {
const errorToast = new CustomEvent('show-toast', {
detail: {
message: evt.detail.error || 'An error occurred while processing your request',
type: 'error'
}
});
window.dispatchEvent(errorToast);
});
document.body.addEventListener('htmx:sendError', function(evt) {
const errorToast = new CustomEvent('show-toast', {
detail: {
message: 'Network error: Could not connect to the server',
type: 'error'
}
});
window.dispatchEvent(errorToast);
});
</script>
</div>
{% endblock %}
{% block extra_js %}
<!-- Base HTMX Configuration -->
<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>
<!-- Custom Moderation JS -->
<script src="{% static 'js/moderation.js' %}"></script>
<!-- Enhanced Mobile Styles -->
<style>
@media (max-width: 640px) {
.action-buttons {
@apply flex-col w-full space-y-2;
}
.action-buttons > button {
@apply w-full justify-center;
}
.search-results {
@apply fixed bottom-0 left-0 right-0 max-h-[50vh] overflow-y-auto bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 rounded-t-xl shadow-xl;
}
.form-grid {
@apply grid-cols-1;
}
}
/* Touch Device Optimizations */
@media (hover: none) {
.touch-target {
@apply min-h-[44px] min-w-[44px] p-2;
}
.touch-friendly-select {
@apply py-2.5;
}
}
</style>
<!-- Accessibility Improvements -->
<div id="a11y-announcer"
class="sr-only"
aria-live="polite"
aria-atomic="true">
</div>
{% endblock %}

View File

@@ -1,69 +1,129 @@
{% load static %}
{% comment %}
This template contains the Alpine.js store for managing filter state in the moderation dashboard
{% endcomment %}
<div x-data
x-init="$store.filters = {
active: [],
labels: {
'submission_type': 'Type',
'content_type': 'Content',
'type': 'Change Type',
'status': 'Status'
},
values: {
'submission_type': {
'text': 'Text',
'photo': 'Photo'
},
'content_type': {
'park': 'Park',
'ride': 'Ride',
'company': 'Company'
},
'type': {
'CREATE': 'New',
'EDIT': 'Edit'
},
'status': {
'PENDING': 'Pending',
'APPROVED': 'Approved',
'REJECTED': 'Rejected',
'ESCALATED': 'Escalated'
}
},
hasActiveFilters: false,
updateActiveFilters() {
const params = new URLSearchParams(window.location.search);
this.active = [];
params.forEach((value, key) => {
if (value && this.labels[key]) {
this.active.push({
name: key,
label: this.labels[key],
value: this.values[key][value] || value
});
}
});
this.hasActiveFilters = this.active.length > 0;
}
};
// Initialize filters from URL
$store.filters.updateActiveFilters();
// Listen for filter changes
window.addEventListener('filter-changed', () => {
$store.filters.updateActiveFilters();
});
// Listen for URL changes
window.addEventListener('htmx:historyRestore', () => {
$store.filters.updateActiveFilters();
});
// Listen for HTMX after swap
window.addEventListener('htmx:afterSwap', () => {
$store.filters.updateActiveFilters();
})">
</div>
<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,69 +1,66 @@
{% load static %}
<div class="space-y-6 animate-pulse">
<!-- Navigation Skeleton -->
<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="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'|make_list %}
{% 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 Section Skeleton -->
<div class="p-6 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="mb-6">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
{% for i in '123'|make_list %}
<div class="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>
<!-- Submissions Skeleton -->
<div class="space-y-4">
{% for i in '123'|make_list %}
<div class="p-6 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">
<div class="w-32 h-6 bg-gray-200 rounded dark:bg-gray-700"></div>
<div class="space-y-2">
{% for j in '1234'|make_list %}
<div class="flex items-center">
<div class="w-5 h-5 mr-2 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 space-y-4">
<!-- Content Details -->
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{% for j in '1234'|make_list %}
<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>
<!-- Action Buttons -->
<div class="flex justify-end gap-3">
{% for j in '1234'|make_list %}
<div class="w-24 h-10 bg-gray-200 rounded-lg dark:bg-gray-700"></div>
{% endfor %}
</div>
</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

@@ -6,10 +6,8 @@
{% endblock %}
{% for submission in submissions %}
<div class="p-4 sm:p-6 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50"
<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 }}"
role="article"
aria-labelledby="submission-header-{{ submission.id }}"
x-data="{
showSuccess: false,
isEditing: false,
@@ -175,13 +173,8 @@
<!-- Edit Mode -->
<form x-show="isEditing"
x-cloak
id="edit-form-{{ submission.id }}"
hx-post="{% url 'moderation:edit_submission' submission.id %}"
hx-target="#submission-{{ submission.id }}"
hx-indicator="#loading-indicator-{{ submission.id }}"
hx-swap="outerHTML"
hx-on::before-request="document.getElementById('edit-form-{{ submission.id }}').classList.add('submitting')"
hx-on::after-request="document.getElementById('edit-form-{{ submission.id }}').classList.remove('submitting')"
class="grid grid-cols-1 gap-3 md:grid-cols-2">
<!-- Location Widget for Parks -->
@@ -231,23 +224,13 @@
<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' %}
<div class="relative">
<input type="date"
id="{{ field }}-{{ submission.id }}"
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 touch-friendly-select"
{% if field == 'closing_date' %}
:required="status === 'CLOSING'"
data-validate="date"
{% endif %}
aria-describedby="{{ field }}-error-{{ submission.id }}"
min="1800-01-01"
max="{{ now|date:'Y-m-d' }}">
<div id="{{ field }}-error-{{ submission.id }}"
class="hidden absolute -bottom-6 left-0 text-sm text-red-600 dark:text-red-400"
role="alert"></div>
</div>
<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">
@@ -356,24 +339,12 @@
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' %}
<div class="relative">
<input type="number"
id="{{ field }}-{{ submission.id }}"
name="{{ field }}"
value="{{ value }}"
min="0"
step="0.1"
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 touch-friendly-select"
placeholder="Height in inches"
aria-describedby="{{ field }}-error-{{ submission.id }}"
data-validate="numeric">
<div id="{{ field }}-error-{{ submission.id }}"
class="hidden absolute -bottom-6 left-0 text-sm text-red-600 dark:text-red-400"
role="alert"></div>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<span class="text-sm text-gray-500 dark:text-gray-400">in</span>
</div>
</div>
<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 }}"
@@ -407,23 +378,14 @@
</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 for="notes-{{ submission.id }}"
class="block mb-2 text-sm font-medium text-blue-900 dark:text-blue-300">
<label class="block mb-2 text-sm font-medium text-blue-900 dark:text-blue-300">
Notes (required):
</label>
<div class="relative">
<textarea id="notes-{{ submission.id }}"
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
aria-required="true"
aria-describedby="notes-error-{{ submission.id }}"
placeholder="Explain why you're editing this submission"></textarea>
<div id="notes-error-{{ submission.id }}"
class="hidden absolute -bottom-6 left-0 text-sm text-red-600 dark:text-red-400"
role="alert"></div>
</div>
<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">
@@ -462,93 +424,52 @@
rows="3"></textarea>
</div>
<div class="flex flex-col sm:flex-row items-stretch sm: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-sm hover:shadow-md touch-target"
@click="showNotes = !showNotes"
aria-expanded="showNotes"
aria-controls="notes-section-{{ submission.id }}">
<i class="mr-2 fas fa-comment-alt" aria-hidden="true"></i>
<span>Add Notes</span>
<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-sm 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-sm hover:shadow-md touch-target"
@click="isEditing = !isEditing"
aria-expanded="isEditing"
aria-controls="edit-form-{{ submission.id }}">
<i class="mr-2 fas fa-edit" aria-hidden="true"></i>
<span>Edit</span>
<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-sm 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-sm hover:shadow-md touch-target disabled:opacity-50 disabled:cursor-not-allowed"
<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-sm hover:shadow-md"
hx-post="{% url 'moderation:approve_submission' submission.id %}"
hx-target="#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-{{ submission.id }}"
hx-disabled-elt="this"
hx-swap="outerHTML"
hx-on::before-request="this.disabled = true"
hx-on::after-request="this.disabled = false"
aria-label="Approve submission">
<i class="mr-2 fas fa-check" aria-hidden="true"></i>
<span>Approve</span>
<span class="htmx-indicator ml-2">
<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>
</span>
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-sm hover:shadow-md touch-target disabled:opacity-50 disabled:cursor-not-allowed"
<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-sm hover:shadow-md"
hx-post="{% url 'moderation:reject_submission' submission.id %}"
hx-target="#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-{{ submission.id }}"
hx-disabled-elt="this"
hx-swap="outerHTML"
hx-on::before-request="this.disabled = true"
hx-on::after-request="this.disabled = false"
aria-label="Reject submission">
<i class="mr-2 fas fa-times" aria-hidden="true"></i>
<span>Reject</span>
<span class="htmx-indicator ml-2">
<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>
</span>
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-sm hover:shadow-md touch-target disabled:opacity-50 disabled:cursor-not-allowed"
<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-sm hover:shadow-md"
hx-post="{% url 'moderation:escalate_submission' submission.id %}"
hx-target="#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-{{ submission.id }}"
hx-disabled-elt="this"
hx-swap="outerHTML"
hx-on::before-request="this.disabled = true"
hx-on::after-request="this.disabled = false"
aria-label="Escalate submission">
<i class="mr-2 fas fa-arrow-up" aria-hidden="true"></i>
<span>Escalate</span>
<span class="htmx-indicator ml-2">
<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>
</span>
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-arrow-up"></i>
Escalate
</button>
{% endif %}
<!-- Submission-specific loading indicator -->
<div id="loading-indicator-{{ submission.id }}"
class="htmx-indicator fixed inset-0 bg-black/20 dark:bg-black/40 flex items-center justify-center z-50"
role="status"
aria-live="polite">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-xl">
<div class="flex items-center gap-3">
<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-100">Processing...</span>
</div>
</div>
</div>
</div>
</div>
{% endif %}