mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:51:09 -05:00
313 lines
8.3 KiB
HTML
313 lines
8.3 KiB
HTML
{% 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 %}
|
|
<!-- 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 }}';
|
|
});
|
|
|
|
// 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 %}
|