Files
thrillwiki_django_no_react/templates/moderation/dashboard.html

336 lines
9.8 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" x-data="moderationDashboard()" @retry-load="retryLoad()">
<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"
@click="$dispatch('retry-load')">
<i class="mr-2 fas fa-sync-alt"></i>
Retry
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
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();
}
});
document.body.addEventListener('htmx:afterOnLoad', (evt) => {
if (evt.detail.target.id === 'dashboard-content') {
this.hideLoadingState();
this.resetFocus(evt.detail.target);
}
});
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 %}