mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:31:09 -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.
|
There was a problem loading the content. Please try again.
|
||||||
</p>
|
</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"
|
<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>
|
<i class="mr-2 fas fa-sync-alt"></i>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
@@ -180,133 +180,156 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% 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>
|
<script>
|
||||||
// HTMX Configuration
|
document.addEventListener('alpine:init', () => {
|
||||||
document.body.addEventListener('htmx:configRequest', function(evt) {
|
// Moderation Dashboard Component
|
||||||
evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
Alpine.data('moderationDashboard', () => ({
|
||||||
});
|
showLoading: false,
|
||||||
|
errorMessage: null,
|
||||||
|
|
||||||
// Loading and Error State Management
|
init() {
|
||||||
const dashboard = {
|
// HTMX Configuration
|
||||||
content: document.getElementById('dashboard-content'),
|
this.setupHTMXConfig();
|
||||||
skeleton: document.getElementById('loading-skeleton'),
|
this.setupEventListeners();
|
||||||
errorState: document.getElementById('error-state'),
|
this.setupSearchDebouncing();
|
||||||
errorMessage: document.getElementById('error-message'),
|
this.setupInfiniteScroll();
|
||||||
|
this.setupKeyboardNavigation();
|
||||||
|
},
|
||||||
|
|
||||||
showLoading() {
|
setupHTMXConfig() {
|
||||||
this.content.setAttribute('aria-busy', 'true');
|
document.body.addEventListener('htmx:configRequest', (evt) => {
|
||||||
this.content.style.opacity = '0';
|
evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||||
this.errorState.classList.add('hidden');
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
hideLoading() {
|
setupEventListeners() {
|
||||||
this.content.setAttribute('aria-busy', 'false');
|
// Enhanced HTMX Event Handlers
|
||||||
this.content.style.opacity = '1';
|
document.body.addEventListener('htmx:beforeRequest', (evt) => {
|
||||||
},
|
if (evt.detail.target.id === 'dashboard-content') {
|
||||||
|
this.showLoadingState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
showError(message) {
|
document.body.addEventListener('htmx:afterOnLoad', (evt) => {
|
||||||
this.errorState.classList.remove('hidden');
|
if (evt.detail.target.id === 'dashboard-content') {
|
||||||
this.errorMessage.textContent = message || 'There was a problem loading the content. Please try again.';
|
this.hideLoadingState();
|
||||||
// Announce error to screen readers
|
this.resetFocus(evt.detail.target);
|
||||||
this.errorMessage.setAttribute('role', 'alert');
|
}
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
// Enhanced HTMX Event Handlers
|
document.body.addEventListener('htmx:responseError', (evt) => {
|
||||||
document.body.addEventListener('htmx:beforeRequest', function(evt) {
|
if (evt.detail.target.id === 'dashboard-content') {
|
||||||
if (evt.detail.target.id === 'dashboard-content') {
|
this.showErrorState(evt.detail.error);
|
||||||
dashboard.showLoading();
|
}
|
||||||
}
|
});
|
||||||
});
|
},
|
||||||
|
|
||||||
document.body.addEventListener('htmx:afterOnLoad', function(evt) {
|
showLoadingState() {
|
||||||
if (evt.detail.target.id === 'dashboard-content') {
|
const content = this.$el.querySelector('#dashboard-content');
|
||||||
dashboard.hideLoading();
|
if (content) {
|
||||||
// Reset focus for accessibility
|
content.setAttribute('aria-busy', 'true');
|
||||||
const firstFocusable = evt.detail.target.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
content.style.opacity = '0';
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Advanced Search Page -->
|
<!-- Advanced Search Page -->
|
||||||
<div class="min-h-screen bg-gradient-to-br from-white via-blue-50/30 to-indigo-50/30 dark:from-gray-950 dark:via-indigo-950/30 dark:to-purple-950/30">
|
<div class="min-h-screen bg-gradient-to-br from-white via-blue-50/30 to-indigo-50/30 dark:from-gray-950 dark:via-indigo-950/30 dark:to-purple-950/30" x-data="advancedSearch()">
|
||||||
|
|
||||||
<!-- Search Header -->
|
<!-- Search Header -->
|
||||||
<section class="py-16 bg-gradient-to-r from-thrill-primary/10 via-purple-500/10 to-pink-500/10 backdrop-blur-sm">
|
<section class="py-16 bg-gradient-to-r from-thrill-primary/10 via-purple-500/10 to-pink-500/10 backdrop-blur-sm">
|
||||||
@@ -238,7 +238,8 @@
|
|||||||
<!-- Clear Filters -->
|
<!-- Clear Filters -->
|
||||||
<button type="button"
|
<button type="button"
|
||||||
id="clear-filters"
|
id="clear-filters"
|
||||||
class="btn-ghost w-full">
|
class="btn-ghost w-full"
|
||||||
|
@click="clearFilters()">
|
||||||
<i class="fas fa-times mr-2"></i>
|
<i class="fas fa-times mr-2"></i>
|
||||||
Clear All Filters
|
Clear All Filters
|
||||||
</button>
|
</button>
|
||||||
@@ -259,10 +260,10 @@
|
|||||||
|
|
||||||
<!-- View Toggle -->
|
<!-- View Toggle -->
|
||||||
<div class="flex items-center space-x-2 bg-white dark:bg-neutral-800 rounded-lg p-1 border border-neutral-200 dark:border-neutral-700">
|
<div class="flex items-center space-x-2 bg-white dark:bg-neutral-800 rounded-lg p-1 border border-neutral-200 dark:border-neutral-700">
|
||||||
<button class="p-2 rounded-md bg-thrill-primary text-white" id="grid-view">
|
<button class="p-2 rounded-md bg-thrill-primary text-white" id="grid-view" @click="setViewMode('grid')">
|
||||||
<i class="fas fa-th-large"></i>
|
<i class="fas fa-th-large"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="p-2 rounded-md text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-700" id="list-view">
|
<button class="p-2 rounded-md text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-700" id="list-view" @click="setViewMode('list')">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,22 +300,150 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AlpineJS Advanced Search Component (HTMX + AlpineJS Only) -->
|
<!-- AlpineJS Advanced Search Component (HTMX + AlpineJS Only) -->
|
||||||
<div x-data="{
|
<script>
|
||||||
searchType: 'parks',
|
document.addEventListener('alpine:init', () => {
|
||||||
viewMode: 'grid',
|
Alpine.data('advancedSearch', () => ({
|
||||||
toggleSearchType(type) {
|
searchType: 'parks',
|
||||||
this.searchType = type;
|
viewMode: 'grid',
|
||||||
},
|
|
||||||
clearFilters() {
|
init() {
|
||||||
document.getElementById('advanced-search-form').reset();
|
// Initialize range sliders
|
||||||
this.searchType = 'parks';
|
this.updateRangeValues();
|
||||||
},
|
this.setupRadioButtons();
|
||||||
setViewMode(mode) {
|
this.setupCheckboxes();
|
||||||
this.viewMode = mode;
|
},
|
||||||
}
|
|
||||||
}" style="display: none;">
|
toggleSearchType(type) {
|
||||||
<!-- Advanced search functionality handled by AlpineJS + HTMX -->
|
this.searchType = type;
|
||||||
</div>
|
const parkFilters = this.$el.querySelector('#park-filters');
|
||||||
|
const rideFilters = this.$el.querySelector('#ride-filters');
|
||||||
|
|
||||||
|
if (type === 'parks') {
|
||||||
|
parkFilters?.classList.remove('hidden');
|
||||||
|
rideFilters?.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
parkFilters?.classList.add('hidden');
|
||||||
|
rideFilters?.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFilters() {
|
||||||
|
const form = this.$el.querySelector('#advanced-search-form');
|
||||||
|
if (form) {
|
||||||
|
form.reset();
|
||||||
|
this.searchType = 'parks';
|
||||||
|
this.toggleSearchType('parks');
|
||||||
|
this.updateRangeValues();
|
||||||
|
this.setupRadioButtons();
|
||||||
|
this.setupCheckboxes();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setViewMode(mode) {
|
||||||
|
this.viewMode = mode;
|
||||||
|
const gridBtn = this.$el.querySelector('#grid-view');
|
||||||
|
const listBtn = this.$el.querySelector('#list-view');
|
||||||
|
const resultsContainer = this.$el.querySelector('#search-results');
|
||||||
|
|
||||||
|
if (mode === 'grid') {
|
||||||
|
gridBtn?.classList.add('bg-thrill-primary', 'text-white');
|
||||||
|
gridBtn?.classList.remove('text-neutral-600', 'dark:text-neutral-400', 'hover:bg-neutral-100', 'dark:hover:bg-neutral-700');
|
||||||
|
listBtn?.classList.remove('bg-thrill-primary', 'text-white');
|
||||||
|
listBtn?.classList.add('text-neutral-600', 'dark:text-neutral-400', 'hover:bg-neutral-100', 'dark:hover:bg-neutral-700');
|
||||||
|
resultsContainer?.classList.remove('list-view');
|
||||||
|
resultsContainer?.classList.add('grid-view');
|
||||||
|
} else {
|
||||||
|
listBtn?.classList.add('bg-thrill-primary', 'text-white');
|
||||||
|
listBtn?.classList.remove('text-neutral-600', 'dark:text-neutral-400', 'hover:bg-neutral-100', 'dark:hover:bg-neutral-700');
|
||||||
|
gridBtn?.classList.remove('bg-thrill-primary', 'text-white');
|
||||||
|
gridBtn?.classList.add('text-neutral-600', 'dark:text-neutral-400', 'hover:bg-neutral-100', 'dark:hover:bg-neutral-700');
|
||||||
|
resultsContainer?.classList.remove('grid-view');
|
||||||
|
resultsContainer?.classList.add('list-view');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRangeValues() {
|
||||||
|
const minRidesSlider = this.$el.querySelector('input[name="min_rides"]');
|
||||||
|
const minRidesValue = this.$el.querySelector('#min-rides-value');
|
||||||
|
const minHeightSlider = this.$el.querySelector('input[name="min_height"]');
|
||||||
|
const minHeightValue = this.$el.querySelector('#min-height-value');
|
||||||
|
const minSpeedSlider = this.$el.querySelector('input[name="min_speed"]');
|
||||||
|
const minSpeedValue = this.$el.querySelector('#min-speed-value');
|
||||||
|
|
||||||
|
if (minRidesSlider && minRidesValue) {
|
||||||
|
minRidesValue.textContent = minRidesSlider.value;
|
||||||
|
minRidesSlider.addEventListener('input', (e) => {
|
||||||
|
minRidesValue.textContent = e.target.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minHeightSlider && minHeightValue) {
|
||||||
|
minHeightValue.textContent = minHeightSlider.value + 'ft';
|
||||||
|
minHeightSlider.addEventListener('input', (e) => {
|
||||||
|
minHeightValue.textContent = e.target.value + 'ft';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minSpeedSlider && minSpeedValue) {
|
||||||
|
minSpeedValue.textContent = minSpeedSlider.value + 'mph';
|
||||||
|
minSpeedSlider.addEventListener('input', (e) => {
|
||||||
|
minSpeedValue.textContent = e.target.value + 'mph';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupRadioButtons() {
|
||||||
|
const radioButtons = this.$el.querySelectorAll('input[type="radio"]');
|
||||||
|
radioButtons.forEach(radio => {
|
||||||
|
const indicator = radio.parentElement.querySelector('div');
|
||||||
|
const dot = indicator?.querySelector('div');
|
||||||
|
|
||||||
|
if (radio.checked && dot) {
|
||||||
|
dot.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
radio.addEventListener('change', () => {
|
||||||
|
// Reset all radio buttons in the same group
|
||||||
|
const groupName = radio.name;
|
||||||
|
const groupRadios = this.$el.querySelectorAll(`input[name="${groupName}"]`);
|
||||||
|
groupRadios.forEach(groupRadio => {
|
||||||
|
const groupIndicator = groupRadio.parentElement.querySelector('div');
|
||||||
|
const groupDot = groupIndicator?.querySelector('div');
|
||||||
|
if (groupDot) {
|
||||||
|
groupDot.style.opacity = groupRadio.checked ? '1' : '0';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (radio.name === 'search_type') {
|
||||||
|
this.toggleSearchType(radio.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setupCheckboxes() {
|
||||||
|
const checkboxes = this.$el.querySelectorAll('input[type="checkbox"]');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
const customCheckbox = checkbox.parentElement.querySelector('.checkbox-custom');
|
||||||
|
|
||||||
|
if (checkbox.checked && customCheckbox) {
|
||||||
|
customCheckbox.classList.add('checked');
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox.addEventListener('change', () => {
|
||||||
|
if (customCheckbox) {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
customCheckbox.classList.add('checked');
|
||||||
|
} else {
|
||||||
|
customCheckbox.classList.remove('checked');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Custom CSS for checkboxes and enhanced styling -->
|
<!-- Custom CSS for checkboxes and enhanced styling -->
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
.status-pending { background: #f59e0b; }
|
.status-pending { background: #f59e0b; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50 p-8">
|
<body class="bg-gray-50 p-8" x-data="authModalTestSuite()">
|
||||||
<div class="max-w-7xl mx-auto">
|
<div class="max-w-7xl mx-auto">
|
||||||
<h1 class="text-3xl font-bold mb-8 text-center">Auth Modal Component Comparison Test</h1>
|
<h1 class="text-3xl font-bold mb-8 text-center">Auth Modal Component Comparison Test</h1>
|
||||||
<p class="text-center text-gray-600 mb-8">Comparing original include method vs new cotton component for Auth Modal with full Alpine.js functionality</p>
|
<p class="text-center text-gray-600 mb-8">Comparing original include method vs new cotton component for Auth Modal with full Alpine.js functionality</p>
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
|
|
||||||
<div class="modal-test-container">
|
<div class="modal-test-container">
|
||||||
<div class="modal-test-group" data-label="Original Include Version">
|
<div class="modal-test-group" data-label="Original Include Version">
|
||||||
<button class="test-button" onclick="if(window.authModalOriginal) window.authModalOriginal.open = true">
|
<button class="test-button" @click="openOriginalModal()">
|
||||||
Open Original Auth Modal
|
Open Original Auth Modal
|
||||||
</button>
|
</button>
|
||||||
<div class="feature-list">
|
<div class="feature-list">
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-test-group" data-label="Cotton Component Version">
|
<div class="modal-test-group" data-label="Cotton Component Version">
|
||||||
<button class="test-button" onclick="if(window.authModalCotton) window.authModalCotton.open = true">
|
<button class="test-button" @click="openCottonModal()">
|
||||||
Open Cotton Auth Modal
|
Open Cotton Auth Modal
|
||||||
</button>
|
</button>
|
||||||
<div class="feature-list">
|
<div class="feature-list">
|
||||||
@@ -161,10 +161,10 @@
|
|||||||
|
|
||||||
<div class="modal-test-container">
|
<div class="modal-test-container">
|
||||||
<div class="modal-test-group" data-label="Original Include Version">
|
<div class="modal-test-group" data-label="Original Include Version">
|
||||||
<button class="test-button" onclick="openOriginalModalInMode('login')">
|
<button class="test-button" @click="openOriginalModalInMode('login')">
|
||||||
Open in Login Mode
|
Open in Login Mode
|
||||||
</button>
|
</button>
|
||||||
<button class="test-button secondary" onclick="openOriginalModalInMode('register')">
|
<button class="test-button secondary" @click="openOriginalModalInMode('register')">
|
||||||
Open in Register Mode
|
Open in Register Mode
|
||||||
</button>
|
</button>
|
||||||
<div class="feature-list">
|
<div class="feature-list">
|
||||||
@@ -181,10 +181,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-test-group" data-label="Cotton Component Version">
|
<div class="modal-test-group" data-label="Cotton Component Version">
|
||||||
<button class="test-button" onclick="openCottonModalInMode('login')">
|
<button class="test-button" @click="openCottonModalInMode('login')">
|
||||||
Open in Login Mode
|
Open in Login Mode
|
||||||
</button>
|
</button>
|
||||||
<button class="test-button secondary" onclick="openCottonModalInMode('register')">
|
<button class="test-button secondary" @click="openCottonModalInMode('register')">
|
||||||
Open in Register Mode
|
Open in Register Mode
|
||||||
</button>
|
</button>
|
||||||
<div class="feature-list">
|
<div class="feature-list">
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
|
|
||||||
<div class="modal-test-container">
|
<div class="modal-test-container">
|
||||||
<div class="modal-test-group" data-label="Original Include Version">
|
<div class="modal-test-group" data-label="Original Include Version">
|
||||||
<button class="test-button" onclick="testOriginalInteractivity()">
|
<button class="test-button" @click="testOriginalInteractivity()">
|
||||||
Test Original Interactions
|
Test Original Interactions
|
||||||
</button>
|
</button>
|
||||||
<div class="feature-list">
|
<div class="feature-list">
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-test-group" data-label="Cotton Component Version">
|
<div class="modal-test-group" data-label="Cotton Component Version">
|
||||||
<button class="test-button" onclick="testCottonInteractivity()">
|
<button class="test-button" @click="testCottonInteractivity()">
|
||||||
Test Cotton Interactions
|
Test Cotton Interactions
|
||||||
</button>
|
</button>
|
||||||
<div class="feature-list">
|
<div class="feature-list">
|
||||||
@@ -251,7 +251,7 @@
|
|||||||
|
|
||||||
<div class="modal-test-container">
|
<div class="modal-test-container">
|
||||||
<div class="modal-test-group" data-label="Styling Verification">
|
<div class="modal-test-group" data-label="Styling Verification">
|
||||||
<button class="test-button" onclick="compareModalStyling()">
|
<button class="test-button" @click="compareModalStyling()">
|
||||||
Compare Both Modals Side by Side
|
Compare Both Modals Side by Side
|
||||||
</button>
|
</button>
|
||||||
<div class="feature-list">
|
<div class="feature-list">
|
||||||
@@ -278,7 +278,7 @@
|
|||||||
|
|
||||||
<div class="modal-test-container">
|
<div class="modal-test-container">
|
||||||
<div class="modal-test-group" data-label="Custom Configuration Test">
|
<div class="modal-test-group" data-label="Custom Configuration Test">
|
||||||
<button class="test-button" onclick="testCustomConfiguration()">
|
<button class="test-button" @click="testCustomConfiguration()">
|
||||||
Test Custom Cotton Config
|
Test Custom Cotton Config
|
||||||
</button>
|
</button>
|
||||||
<div class="feature-list">
|
<div class="feature-list">
|
||||||
@@ -439,73 +439,89 @@
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store references to both modal instances
|
// Auth Modal Test Suite Component
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
Alpine.data('authModalTestSuite', () => ({
|
||||||
// Wait for Alpine.js to initialize and modal instances to be created
|
init() {
|
||||||
setTimeout(() => {
|
// Wait for Alpine.js to initialize and modal instances to be created
|
||||||
// Both modals should now be available with their respective window keys
|
setTimeout(() => {
|
||||||
console.log('Auth Modal References:', {
|
console.log('Auth Modal References:', {
|
||||||
original: window.authModalOriginal,
|
original: window.authModalOriginal,
|
||||||
cotton: window.authModalCotton,
|
cotton: window.authModalCotton,
|
||||||
custom: window.authModalCustom
|
custom: window.authModalCustom
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
},
|
||||||
|
|
||||||
// Test functions
|
openOriginalModal() {
|
||||||
function openOriginalModalInMode(mode) {
|
if (window.authModalOriginal) {
|
||||||
if (window.authModalOriginal) {
|
window.authModalOriginal.open = true;
|
||||||
window.authModalOriginal.mode = mode;
|
}
|
||||||
window.authModalOriginal.open = true;
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openCottonModalInMode(mode) {
|
openCottonModal() {
|
||||||
if (window.authModalCotton) {
|
if (window.authModalCotton) {
|
||||||
window.authModalCotton.mode = mode;
|
window.authModalCotton.open = true;
|
||||||
window.authModalCotton.open = true;
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
function testOriginalInteractivity() {
|
openOriginalModalInMode(mode) {
|
||||||
if (window.authModalOriginal) {
|
if (window.authModalOriginal) {
|
||||||
window.authModalOriginal.open = true;
|
window.authModalOriginal.mode = mode;
|
||||||
window.authModalOriginal.mode = 'login';
|
window.authModalOriginal.open = true;
|
||||||
setTimeout(() => {
|
}
|
||||||
window.authModalOriginal.loginError = 'Test error message';
|
},
|
||||||
window.authModalOriginal.showPassword = true;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function testCottonInteractivity() {
|
openCottonModalInMode(mode) {
|
||||||
if (window.authModalCotton) {
|
if (window.authModalCotton) {
|
||||||
window.authModalCotton.open = true;
|
window.authModalCotton.mode = mode;
|
||||||
window.authModalCotton.mode = 'login';
|
window.authModalCotton.open = true;
|
||||||
setTimeout(() => {
|
}
|
||||||
window.authModalCotton.loginError = 'Test error message';
|
},
|
||||||
window.authModalCotton.showPassword = true;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareModalStyling() {
|
testOriginalInteractivity() {
|
||||||
if (window.authModalOriginal && window.authModalCotton) {
|
if (window.authModalOriginal) {
|
||||||
window.authModalOriginal.open = true;
|
window.authModalOriginal.open = true;
|
||||||
setTimeout(() => {
|
window.authModalOriginal.mode = 'login';
|
||||||
window.authModalCotton.open = true;
|
setTimeout(() => {
|
||||||
}, 200);
|
window.authModalOriginal.loginError = 'Test error message';
|
||||||
}
|
window.authModalOriginal.showPassword = true;
|
||||||
}
|
}, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
function testCustomConfiguration() {
|
testCottonInteractivity() {
|
||||||
// Show the custom cotton modal
|
if (window.authModalCotton) {
|
||||||
const customModal = document.getElementById('custom-cotton-modal');
|
window.authModalCotton.open = true;
|
||||||
customModal.style.display = 'block';
|
window.authModalCotton.mode = 'login';
|
||||||
|
setTimeout(() => {
|
||||||
|
window.authModalCotton.loginError = 'Test error message';
|
||||||
|
window.authModalCotton.showPassword = true;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// You would implement custom Alpine.js instance here
|
compareModalStyling() {
|
||||||
alert('Custom configuration test - check the modal titles and text changes');
|
if (window.authModalOriginal && window.authModalCotton) {
|
||||||
}
|
window.authModalOriginal.open = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
window.authModalCotton.open = true;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
testCustomConfiguration() {
|
||||||
|
// Show the custom cotton modal
|
||||||
|
const customModal = this.$el.querySelector('#custom-cotton-modal');
|
||||||
|
if (customModal) {
|
||||||
|
customModal.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch custom event for configuration test
|
||||||
|
this.$dispatch('custom-config-test', {
|
||||||
|
message: 'Custom configuration test - check the modal titles and text changes'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50 p-8">
|
<body class="bg-gray-50 p-8" x-data="componentTestSuite()">
|
||||||
<div class="max-w-6xl mx-auto">
|
<div class="max-w-6xl mx-auto">
|
||||||
<h1 class="text-3xl font-bold mb-8 text-center">UI Component Comparison Test</h1>
|
<h1 class="text-3xl font-bold mb-8 text-center">UI Component Comparison Test</h1>
|
||||||
<p class="text-center text-gray-600 mb-8">Comparing old include method vs new cotton component method for Button, Input, and Card components</p>
|
<p class="text-center text-gray-600 mb-8">Comparing old include method vs new cotton component method for Button, Input, and Card components</p>
|
||||||
@@ -582,72 +582,94 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Alpine.js -->
|
||||||
|
<script src="{% static 'js/alpine.min.js' %}" defer></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Function to normalize HTML for comparison
|
document.addEventListener('alpine:init', () => {
|
||||||
function normalizeHTML(html) {
|
// Component Test Suite Component
|
||||||
return html
|
Alpine.data('componentTestSuite', () => ({
|
||||||
.replace(/\s+/g, ' ')
|
init() {
|
||||||
.replace(/> </g, '><')
|
// Extract HTML after Alpine.js initializes
|
||||||
.trim();
|
this.$nextTick(() => {
|
||||||
}
|
setTimeout(() => this.extractComponentHTML(), 100);
|
||||||
|
this.addCompareButton();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Function to extract HTML from all component containers
|
// Function to normalize HTML for comparison
|
||||||
function extractComponentHTML() {
|
normalizeHTML(html) {
|
||||||
const containers = document.querySelectorAll('.button-container');
|
return html
|
||||||
const includeHTMLs = [];
|
.replace(/\s+/g, ' ')
|
||||||
const cottonHTMLs = [];
|
.replace(/> </g, '><')
|
||||||
let componentIndex = 1;
|
.trim();
|
||||||
|
},
|
||||||
|
|
||||||
containers.forEach((container, index) => {
|
// Function to extract HTML from all component containers
|
||||||
const label = container.getAttribute('data-label');
|
extractComponentHTML() {
|
||||||
// Look for button, input, or div (card) elements
|
const containers = this.$el.querySelectorAll('.button-container');
|
||||||
const element = container.querySelector('button') ||
|
const includeHTMLs = [];
|
||||||
container.querySelector('input') ||
|
const cottonHTMLs = [];
|
||||||
container.querySelector('div.rounded-lg');
|
let componentIndex = 1;
|
||||||
|
|
||||||
if (element && label) {
|
containers.forEach((container, index) => {
|
||||||
const html = element.outerHTML;
|
const label = container.getAttribute('data-label');
|
||||||
const normalized = normalizeHTML(html);
|
// Look for button, input, or div (card) elements
|
||||||
|
const element = container.querySelector('button') ||
|
||||||
|
container.querySelector('input') ||
|
||||||
|
container.querySelector('div.rounded-lg');
|
||||||
|
|
||||||
if (label === 'Include Version') {
|
if (element && label) {
|
||||||
includeHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
|
const html = element.outerHTML;
|
||||||
} else if (label === 'Cotton Version') {
|
const normalized = this.normalizeHTML(html);
|
||||||
cottonHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
|
|
||||||
componentIndex++;
|
if (label === 'Include Version') {
|
||||||
|
includeHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
|
||||||
|
} else if (label === 'Cotton Version') {
|
||||||
|
cottonHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
|
||||||
|
componentIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const includeElement = this.$el.querySelector('#include-html');
|
||||||
|
const cottonElement = this.$el.querySelector('#cotton-html');
|
||||||
|
|
||||||
|
if (includeElement) includeElement.textContent = includeHTMLs.join('\n');
|
||||||
|
if (cottonElement) cottonElement.textContent = cottonHTMLs.join('\n');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to compare HTML outputs
|
||||||
|
compareHTML() {
|
||||||
|
const includeHTML = this.$el.querySelector('#include-html')?.textContent || '';
|
||||||
|
const cottonHTML = this.$el.querySelector('#cotton-html')?.textContent || '';
|
||||||
|
|
||||||
|
if (includeHTML === cottonHTML) {
|
||||||
|
this.$dispatch('comparison-result', {
|
||||||
|
success: true,
|
||||||
|
message: '✅ HTML outputs are identical!'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$dispatch('comparison-result', {
|
||||||
|
success: false,
|
||||||
|
message: '❌ HTML outputs differ. Check the HTML Output section for details.',
|
||||||
|
includeHTML,
|
||||||
|
cottonHTML
|
||||||
|
});
|
||||||
|
console.log('Include HTML:', includeHTML);
|
||||||
|
console.log('Cotton HTML:', cottonHTML);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add compare button
|
||||||
|
addCompareButton() {
|
||||||
|
const compareBtn = document.createElement('button');
|
||||||
|
compareBtn.textContent = 'Compare HTML Outputs';
|
||||||
|
compareBtn.className = 'fixed bottom-4 right-4 bg-blue-500 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-600';
|
||||||
|
compareBtn.addEventListener('click', () => this.compareHTML());
|
||||||
|
document.body.appendChild(compareBtn);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
document.getElementById('include-html').textContent = includeHTMLs.join('\n');
|
|
||||||
document.getElementById('cotton-html').textContent = cottonHTMLs.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract HTML after page loads
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
setTimeout(extractComponentHTML, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to compare HTML outputs
|
|
||||||
function compareHTML() {
|
|
||||||
const includeHTML = document.getElementById('include-html').textContent;
|
|
||||||
const cottonHTML = document.getElementById('cotton-html').textContent;
|
|
||||||
|
|
||||||
if (includeHTML === cottonHTML) {
|
|
||||||
alert('✅ HTML outputs are identical!');
|
|
||||||
} else {
|
|
||||||
alert('❌ HTML outputs differ. Check the HTML Output section for details.');
|
|
||||||
console.log('Include HTML:', includeHTML);
|
|
||||||
console.log('Cotton HTML:', cottonHTML);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add compare button
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const compareBtn = document.createElement('button');
|
|
||||||
compareBtn.textContent = 'Compare HTML Outputs';
|
|
||||||
compareBtn.className = 'fixed bottom-4 right-4 bg-blue-500 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-600';
|
|
||||||
compareBtn.onclick = compareHTML;
|
|
||||||
document.body.appendChild(compareBtn);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user