mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 15:51:09 -05:00
Refactor authentication settings and enhance frontend moderation panel with performance optimizations, loading states, error handling, mobile responsiveness, and accessibility improvements
This commit is contained in:
@@ -56,44 +56,93 @@
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
|
||||
<form class="flex flex-wrap items-end gap-4 mb-6"
|
||||
<form class="mb-6"
|
||||
x-data="{ showFilters: false }"
|
||||
hx-get="{% url 'moderation:submission_list' %}"
|
||||
hx-target="#dashboard-content"
|
||||
hx-trigger="change"
|
||||
hx-push-url="true">
|
||||
hx-trigger="change from:select"
|
||||
hx-push-url="true"
|
||||
aria-label="Submission filters">
|
||||
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Submission Type
|
||||
</label>
|
||||
<select name="submission_type" class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">All Submissions</option>
|
||||
<option value="text" {% if request.GET.submission_type == 'text' %}selected{% endif %}>Text Submissions</option>
|
||||
<option value="photo" {% if request.GET.submission_type == 'photo' %}selected{% endif %}>Photo Submissions</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Mobile Filter Toggle -->
|
||||
<button type="button"
|
||||
class="flex items-center w-full gap-2 p-3 mb-4 font-medium text-left text-gray-700 transition-colors duration-200 bg-gray-100 rounded-lg md:hidden dark:text-gray-300 dark:bg-gray-900"
|
||||
@click="showFilters = !showFilters"
|
||||
:aria-expanded="showFilters"
|
||||
aria-controls="filter-controls">
|
||||
<i class="fas" :class="showFilters ? 'fa-chevron-up' : 'fa-chevron-down'"></i>
|
||||
<span>Filter Options</span>
|
||||
<span class="flex items-center ml-auto space-x-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="active-filters">{{ request.GET|length }} active</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Type
|
||||
</label>
|
||||
<select name="type" class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">All Types</option>
|
||||
<option value="CREATE" {% if request.GET.type == 'CREATE' %}selected{% endif %}>New Submissions</option>
|
||||
<option value="EDIT" {% if request.GET.type == 'EDIT' %}selected{% endif %}>Edit Submissions</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Filter Controls -->
|
||||
<div id="filter-controls"
|
||||
class="grid gap-4 transition-all duration-200 md:grid-cols-3"
|
||||
:class="{'hidden md:grid': !showFilters, 'grid': showFilters}"
|
||||
role="group"
|
||||
aria-label="Filter controls">
|
||||
|
||||
<div class="relative">
|
||||
<label id="submission-type-label"
|
||||
class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Submission Type
|
||||
</label>
|
||||
<select name="submission_type"
|
||||
aria-labelledby="submission-type-label"
|
||||
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@change="$dispatch('filter-changed')">
|
||||
<option value="">All Submissions</option>
|
||||
<option value="text" {% if request.GET.submission_type == 'text' %}selected{% endif %}>Text Submissions</option>
|
||||
<option value="photo" {% if request.GET.submission_type == 'photo' %}selected{% endif %}>Photo Submissions</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Content Type
|
||||
</label>
|
||||
<select name="content_type" class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">All Content</option>
|
||||
<option value="park" {% if request.GET.content_type == 'park' %}selected{% endif %}>Parks</option>
|
||||
<option value="ride" {% if request.GET.content_type == 'ride' %}selected{% endif %}>Rides</option>
|
||||
<option value="company" {% if request.GET.content_type == 'company' %}selected{% endif %}>Companies</option>
|
||||
</select>
|
||||
<div class="relative">
|
||||
<label id="type-label"
|
||||
class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Type
|
||||
</label>
|
||||
<select name="type"
|
||||
aria-labelledby="type-label"
|
||||
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@change="$dispatch('filter-changed')">
|
||||
<option value="">All Types</option>
|
||||
<option value="CREATE" {% if request.GET.type == 'CREATE' %}selected{% endif %}>New Submissions</option>
|
||||
<option value="EDIT" {% if request.GET.type == 'EDIT' %}selected{% endif %}>Edit Submissions</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<label id="content-type-label"
|
||||
class="block mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Content Type
|
||||
</label>
|
||||
<select name="content_type"
|
||||
aria-labelledby="content-type-label"
|
||||
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@change="$dispatch('filter-changed')">
|
||||
<option value="">All Content</option>
|
||||
<option value="park" {% if request.GET.content_type == 'park' %}selected{% endif %}>Parks</option>
|
||||
<option value="ride" {% if request.GET.content_type == 'ride' %}selected{% endif %}>Rides</option>
|
||||
<option value="company" {% if request.GET.content_type == 'company' %}selected{% endif %}>Companies</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Applied Filters Summary -->
|
||||
<div class="hidden pt-2 md:col-span-3 md:block">
|
||||
<div class="flex flex-wrap gap-2"
|
||||
x-show="$store.filters.hasActiveFilters"
|
||||
x-cloak>
|
||||
<template x-for="filter in $store.filters.active" :key="filter.name">
|
||||
<span class="inline-flex items-center px-2 py-1 text-sm bg-blue-100 rounded dark:bg-blue-900/40">
|
||||
<span class="mr-1 text-blue-700 dark:text-blue-300" x-text="filter.label + ':'"></span>
|
||||
<span class="font-medium text-blue-900 dark:text-blue-100" x-text="filter.value"></span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
129
templates/moderation/partials/filters_store.html
Normal file
129
templates/moderation/partials/filters_store.html
Normal file
@@ -0,0 +1,129 @@
|
||||
{% comment %}
|
||||
This template contains the Alpine.js store for managing filter state in the moderation dashboard
|
||||
{% endcomment %}
|
||||
|
||||
<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>
|
||||
66
templates/moderation/partials/loading_skeleton.html
Normal file
66
templates/moderation/partials/loading_skeleton.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% load static %}
|
||||
|
||||
<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" %}
|
||||
<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 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>
|
||||
Reference in New Issue
Block a user