mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:11:08 -05:00
Refactor moderation dashboard and advanced search components to utilize Alpine.js for improved state management. Enhanced event handling and user experience by replacing legacy JavaScript functions with Alpine.js reactive methods. Updated auth modal comparison and button comparison tests to leverage Alpine.js for better interactivity and functionality.
This commit is contained in:
@@ -169,7 +169,7 @@
|
||||
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()">
|
||||
@click="$dispatch('retry-load')">
|
||||
<i class="mr-2 fas fa-sync-alt"></i>
|
||||
Retry
|
||||
</button>
|
||||
@@ -180,133 +180,156 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- AlpineJS Moderation Dashboard Component (HTMX + AlpineJS Only) -->
|
||||
<div x-data="{
|
||||
showLoading: false,
|
||||
errorMessage: null,
|
||||
showError(message) {
|
||||
this.errorMessage = message;
|
||||
}
|
||||
}"
|
||||
@htmx:before-request="showLoading = true"
|
||||
@htmx:after-request="showLoading = false"
|
||||
@htmx:response-error="showError('Failed to load content')"
|
||||
style="display: none;">
|
||||
<!-- Dashboard functionality handled by AlpineJS + HTMX -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// HTMX Configuration
|
||||
document.body.addEventListener('htmx:configRequest', function(evt) {
|
||||
evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||
});
|
||||
document.addEventListener('alpine:init', () => {
|
||||
// Moderation Dashboard Component
|
||||
Alpine.data('moderationDashboard', () => ({
|
||||
showLoading: false,
|
||||
errorMessage: null,
|
||||
|
||||
init() {
|
||||
// HTMX Configuration
|
||||
this.setupHTMXConfig();
|
||||
this.setupEventListeners();
|
||||
this.setupSearchDebouncing();
|
||||
this.setupInfiniteScroll();
|
||||
this.setupKeyboardNavigation();
|
||||
},
|
||||
|
||||
setupHTMXConfig() {
|
||||
document.body.addEventListener('htmx:configRequest', (evt) => {
|
||||
evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||
});
|
||||
},
|
||||
|
||||
setupEventListeners() {
|
||||
// Enhanced HTMX Event Handlers
|
||||
document.body.addEventListener('htmx:beforeRequest', (evt) => {
|
||||
if (evt.detail.target.id === 'dashboard-content') {
|
||||
this.showLoadingState();
|
||||
}
|
||||
});
|
||||
|
||||
// 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'),
|
||||
document.body.addEventListener('htmx:afterOnLoad', (evt) => {
|
||||
if (evt.detail.target.id === 'dashboard-content') {
|
||||
this.hideLoadingState();
|
||||
this.resetFocus(evt.detail.target);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
document.body.addEventListener('htmx:responseError', (evt) => {
|
||||
if (evt.detail.target.id === 'dashboard-content') {
|
||||
this.showErrorState(evt.detail.error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showLoadingState() {
|
||||
const content = this.$el.querySelector('#dashboard-content');
|
||||
if (content) {
|
||||
content.setAttribute('aria-busy', 'true');
|
||||
content.style.opacity = '0';
|
||||
}
|
||||
});
|
||||
}
|
||||
const errorState = this.$el.querySelector('#error-state');
|
||||
if (errorState) {
|
||||
errorState.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
hideLoadingState() {
|
||||
const content = this.$el.querySelector('#dashboard-content');
|
||||
if (content) {
|
||||
content.setAttribute('aria-busy', 'false');
|
||||
content.style.opacity = '1';
|
||||
}
|
||||
},
|
||||
|
||||
showErrorState(message) {
|
||||
const errorState = this.$el.querySelector('#error-state');
|
||||
const errorMessage = this.$el.querySelector('#error-message');
|
||||
|
||||
if (errorState) {
|
||||
errorState.classList.remove('hidden');
|
||||
}
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = message || 'There was a problem loading the content. Please try again.';
|
||||
errorMessage.setAttribute('role', 'alert');
|
||||
}
|
||||
},
|
||||
|
||||
resetFocus(target) {
|
||||
const firstFocusable = target.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (firstFocusable) {
|
||||
firstFocusable.focus();
|
||||
}
|
||||
},
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
setupSearchDebouncing() {
|
||||
const searchInputs = this.$el.querySelectorAll('[data-search]');
|
||||
searchInputs.forEach(input => {
|
||||
const originalSearch = () => {
|
||||
htmx.trigger(input, 'input');
|
||||
};
|
||||
const debouncedSearch = this.debounce(originalSearch, 300);
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
e.preventDefault();
|
||||
debouncedSearch();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setupInfiniteScroll() {
|
||||
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);
|
||||
const infiniteScrollElements = this.$el.querySelectorAll('[data-infinite-scroll]');
|
||||
infiniteScrollElements.forEach(el => observer.observe(el));
|
||||
},
|
||||
|
||||
setupKeyboardNavigation() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const openModals = this.$el.querySelectorAll('[x-show="showNotes"]');
|
||||
openModals.forEach(modal => {
|
||||
const alpineData = modal.__x?.$data;
|
||||
if (alpineData && alpineData.showNotes) {
|
||||
alpineData.showNotes = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
retryLoad() {
|
||||
window.location.reload();
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user