mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:11:09 -05:00
Add version control system functionality with branch management, history tracking, and merge operations
This commit is contained in:
223
static/js/moderation.js
Normal file
223
static/js/moderation.js
Normal file
@@ -0,0 +1,223 @@
|
||||
// Validation Helpers
|
||||
const ValidationRules = {
|
||||
date: {
|
||||
validate: (value, input) => {
|
||||
if (!value) return true;
|
||||
const date = new Date(value);
|
||||
const now = new Date();
|
||||
const min = new Date('1800-01-01');
|
||||
|
||||
if (date > now) {
|
||||
return 'Date cannot be in the future';
|
||||
}
|
||||
if (date < min) {
|
||||
return 'Date cannot be before 1800';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
numeric: {
|
||||
validate: (value, input) => {
|
||||
if (!value) return true;
|
||||
const num = parseFloat(value);
|
||||
const min = parseFloat(input.getAttribute('min') || '-Infinity');
|
||||
const max = parseFloat(input.getAttribute('max') || 'Infinity');
|
||||
|
||||
if (isNaN(num)) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
if (num < min) {
|
||||
return `Value must be at least ${min}`;
|
||||
}
|
||||
if (num > max) {
|
||||
return `Value must be no more than ${max}`;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Form Validation and Error Handling
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Form Validation
|
||||
document.querySelectorAll('form[hx-post]').forEach(form => {
|
||||
// Add validation on field change
|
||||
form.addEventListener('input', function(e) {
|
||||
const input = e.target;
|
||||
if (input.hasAttribute('data-validate')) {
|
||||
validateField(input);
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('htmx:beforeRequest', function(event) {
|
||||
let isValid = true;
|
||||
|
||||
// Validate all fields
|
||||
form.querySelectorAll('[data-validate]').forEach(input => {
|
||||
if (!validateField(input)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Check required notes field
|
||||
const notesField = form.querySelector('textarea[name="notes"]');
|
||||
if (notesField && !notesField.value.trim()) {
|
||||
showError(notesField, 'Notes are required');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
event.preventDefault();
|
||||
// Focus first invalid field
|
||||
form.querySelector('.border-red-500')?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Clear error states on input
|
||||
form.addEventListener('input', function(e) {
|
||||
if (e.target.classList.contains('border-red-500')) {
|
||||
e.target.classList.remove('border-red-500');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Form State Management
|
||||
document.querySelectorAll('form[hx-post]').forEach(form => {
|
||||
const formId = form.getAttribute('id');
|
||||
if (!formId) return;
|
||||
|
||||
// Save form state before submission
|
||||
form.addEventListener('htmx:beforeRequest', function() {
|
||||
const formData = new FormData(form);
|
||||
const state = {};
|
||||
formData.forEach((value, key) => {
|
||||
state[key] = value;
|
||||
});
|
||||
sessionStorage.setItem('formState-' + formId, JSON.stringify(state));
|
||||
});
|
||||
|
||||
// Restore form state if available
|
||||
const savedState = sessionStorage.getItem('formState-' + formId);
|
||||
if (savedState) {
|
||||
const state = JSON.parse(savedState);
|
||||
Object.entries(state).forEach(([key, value]) => {
|
||||
const input = form.querySelector(`[name="${key}"]`);
|
||||
if (input) {
|
||||
input.value = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Park Area Sync with Park Selection
|
||||
document.querySelectorAll('[id^="park-input-"]').forEach(parkInput => {
|
||||
const submissionId = parkInput.id.replace('park-input-', '');
|
||||
const areaSelect = document.querySelector(`#park-area-select-${submissionId}`);
|
||||
|
||||
if (parkInput && areaSelect) {
|
||||
parkInput.addEventListener('change', function() {
|
||||
const parkId = this.value;
|
||||
if (!parkId) {
|
||||
areaSelect.innerHTML = '<option value="">Select area</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
htmx.ajax('GET', `/parks/${parkId}/areas/`, {
|
||||
target: areaSelect,
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Improved Error Handling
|
||||
document.body.addEventListener('htmx:responseError', function(evt) {
|
||||
const errorToast = document.createElement('div');
|
||||
errorToast.className = 'fixed bottom-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 flex items-center';
|
||||
errorToast.innerHTML = `
|
||||
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||
<span>${evt.detail.error || 'An error occurred'}</span>
|
||||
<button class="ml-4 hover:text-red-200" onclick="this.parentElement.remove()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
document.body.appendChild(errorToast);
|
||||
setTimeout(() => {
|
||||
errorToast.remove();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
// Accessibility Improvements
|
||||
document.addEventListener('htmx:afterSettle', function(evt) {
|
||||
// Focus management
|
||||
const target = evt.detail.target;
|
||||
const focusableElement = target.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusableElement) {
|
||||
focusableElement.focus();
|
||||
}
|
||||
|
||||
// Announce state changes
|
||||
if (target.hasAttribute('aria-live')) {
|
||||
const announcement = target.getAttribute('aria-label') || target.textContent;
|
||||
const announcer = document.getElementById('a11y-announcer') || createAnnouncer();
|
||||
announcer.textContent = announcement;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to create accessibility announcer
|
||||
function createAnnouncer() {
|
||||
const announcer = document.createElement('div');
|
||||
announcer.id = 'a11y-announcer';
|
||||
announcer.className = 'sr-only';
|
||||
announcer.setAttribute('aria-live', 'polite');
|
||||
document.body.appendChild(announcer);
|
||||
return announcer;
|
||||
}
|
||||
|
||||
// Validation Helper Functions
|
||||
function validateField(input) {
|
||||
const validationType = input.getAttribute('data-validate');
|
||||
if (!validationType || !ValidationRules[validationType]) return true;
|
||||
|
||||
const result = ValidationRules[validationType].validate(input.value, input);
|
||||
if (result === true) {
|
||||
clearError(input);
|
||||
return true;
|
||||
} else {
|
||||
showError(input, result);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function showError(input, message) {
|
||||
const errorId = input.getAttribute('aria-describedby');
|
||||
const errorElement = document.getElementById(errorId);
|
||||
|
||||
input.classList.add('border-red-500', 'error-shake');
|
||||
if (errorElement) {
|
||||
errorElement.textContent = message;
|
||||
errorElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Announce error to screen readers
|
||||
const announcer = document.getElementById('a11y-announcer');
|
||||
if (announcer) {
|
||||
announcer.textContent = `Error: ${message}`;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
input.classList.remove('error-shake');
|
||||
}, 820);
|
||||
}
|
||||
|
||||
function clearError(input) {
|
||||
const errorId = input.getAttribute('aria-describedby');
|
||||
const errorElement = document.getElementById(errorId);
|
||||
|
||||
input.classList.remove('border-red-500');
|
||||
if (errorElement) {
|
||||
errorElement.classList.add('hidden');
|
||||
errorElement.textContent = '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user