mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-30 10:27:04 -05:00
Compare commits
5 Commits
5b7b203619
...
c437ddbf28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c437ddbf28 | ||
|
|
f7b1296263 | ||
|
|
e53414d795 | ||
|
|
2328c919c9 | ||
|
|
09e2c69493 |
@@ -49,4 +49,6 @@ tailwindcss, django, django-cotton, htmx, alpinejs, django-rest-framework, postg
|
||||
- All models inherit TrackedModel
|
||||
- Real database data only (NO MOCKING)
|
||||
- RichChoiceField over Django choices
|
||||
- Progressive enhancement required
|
||||
- Progressive enhancement required
|
||||
|
||||
YOU ARE STRICTLY AND ABSOLUTELY FORBIDDEN FROM IGNORING, BYPASSING, OR AVOIDING THESE RULES IN ANY WAY WITH NO EXCEPTIONS!!!
|
||||
@@ -145,7 +145,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container max-w-6xl px-4 py-6 mx-auto">
|
||||
<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" %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -1,9 +1,32 @@
|
||||
<div class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('parkSearchResults', () => ({
|
||||
selectPark(id, name) {
|
||||
// Update park fields using AlpineJS reactive approach
|
||||
const parkInput = this.$el.closest('form').querySelector('#id_park');
|
||||
const searchInput = this.$el.closest('form').querySelector('#id_park_search');
|
||||
const resultsDiv = this.$el.closest('form').querySelector('#park-search-results');
|
||||
|
||||
if (parkInput) parkInput.value = id;
|
||||
if (searchInput) searchInput.value = name;
|
||||
if (resultsDiv) resultsDiv.innerHTML = '';
|
||||
|
||||
// Dispatch custom event for parent component
|
||||
this.$dispatch('park-selected', { id, name });
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<div x-data="parkSearchResults()"
|
||||
@click.outside="$el.innerHTML = ''"
|
||||
class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600"
|
||||
style="max-height: 240px; overflow-y: auto;">
|
||||
{% if parks %}
|
||||
{% for park in parks %}
|
||||
<button type="button"
|
||||
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
|
||||
onclick="selectPark('{{ park.id }}', '{{ park.name|escapejs }}')">
|
||||
@click="selectPark('{{ park.id }}', '{{ park.name|escapejs }}')">
|
||||
{{ park.name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
@@ -17,11 +40,3 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function selectPark(id, name) {
|
||||
document.getElementById('id_park').value = id;
|
||||
document.getElementById('id_park_search').value = name;
|
||||
document.getElementById('park-search-results').innerHTML = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -173,22 +173,39 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Search Suggestions -->
|
||||
<div class="flex flex-wrap gap-2 justify-center">
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('searchSuggestions', () => ({
|
||||
fillSearchInput(value) {
|
||||
// Find the search input using AlpineJS approach
|
||||
const searchInput = document.querySelector('input[type=text]');
|
||||
if (searchInput) {
|
||||
searchInput.value = value;
|
||||
// Dispatch input event to trigger search
|
||||
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
searchInput.focus();
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<div x-data="searchSuggestions()" class="flex flex-wrap gap-2 justify-center">
|
||||
<span class="text-xs text-muted-foreground">Try:</span>
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors"
|
||||
onclick="document.querySelector('input[type=text]').value='Disney'; document.querySelector('input[type=text]').dispatchEvent(new Event('input'));">
|
||||
@click="fillSearchInput('Disney')">
|
||||
Disney
|
||||
</button>
|
||||
<span class="text-xs text-muted-foreground">•</span>
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors"
|
||||
onclick="document.querySelector('input[type=text]').value='roller coaster'; document.querySelector('input[type=text]').dispatchEvent(new Event('input'));">
|
||||
@click="fillSearchInput('roller coaster')">
|
||||
Roller Coaster
|
||||
</button>
|
||||
<span class="text-xs text-muted-foreground">•</span>
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors"
|
||||
onclick="document.querySelector('input[type=text]').value='Cedar Point'; document.querySelector('input[type=text]').dispatchEvent(new Event('input'));">
|
||||
@click="fillSearchInput('Cedar Point')">
|
||||
Cedar Point
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,35 +1,66 @@
|
||||
{% load static %}
|
||||
|
||||
<form method="post"
|
||||
class="space-y-6"
|
||||
x-data="{ submitting: false }"
|
||||
@submit.prevent="
|
||||
if (!submitting) {
|
||||
submitting = true;
|
||||
const formData = new FormData($event.target);
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('designerForm', () => ({
|
||||
submitting: false,
|
||||
|
||||
init() {
|
||||
// Listen for HTMX events on this form
|
||||
this.$el.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.pathInfo.requestPath === '/rides/designers/create/') {
|
||||
this.handleResponse(event);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async submitForm(event) {
|
||||
if (this.submitting) return;
|
||||
|
||||
this.submitting = true;
|
||||
const formData = new FormData(event.target);
|
||||
const csrfToken = this.$el.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
// Use HTMX for form submission
|
||||
htmx.ajax('POST', '/rides/designers/create/', {
|
||||
values: Object.fromEntries(formData),
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
target: this.$el,
|
||||
swap: 'none'
|
||||
});
|
||||
},
|
||||
|
||||
handleResponse(event) {
|
||||
this.submitting = false;
|
||||
|
||||
// Handle HTMX response using event listeners
|
||||
document.addEventListener('htmx:afterRequest', function handleResponse(event) {
|
||||
if (event.detail.pathInfo.requestPath === '/rides/designers/create/') {
|
||||
document.removeEventListener('htmx:afterRequest', handleResponse);
|
||||
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
const data = JSON.parse(event.detail.xhr.response);
|
||||
if (typeof selectDesigner === 'function') {
|
||||
selectDesigner(data.id, data.name);
|
||||
}
|
||||
$dispatch('close-designer-modal');
|
||||
}
|
||||
submitting = false;
|
||||
}
|
||||
});
|
||||
}">
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
const data = JSON.parse(event.detail.xhr.response);
|
||||
|
||||
// Dispatch event with designer data for parent components
|
||||
this.$dispatch('designer-created', {
|
||||
id: data.id,
|
||||
name: data.name
|
||||
});
|
||||
|
||||
// Close modal if in modal context
|
||||
this.$dispatch('close-designer-modal');
|
||||
} else {
|
||||
// Handle error case
|
||||
this.$dispatch('designer-creation-error', {
|
||||
error: event.detail.xhr.responseText
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<form method="post"
|
||||
class="space-y-6"
|
||||
x-data="designerForm()"
|
||||
@submit.prevent="submitForm($event)">
|
||||
{% csrf_token %}
|
||||
|
||||
<div id="designer-form-notification"></div>
|
||||
|
||||
@@ -1,35 +1,66 @@
|
||||
{% load static %}
|
||||
|
||||
<form method="post"
|
||||
class="space-y-6"
|
||||
x-data="{ submitting: false }"
|
||||
@submit.prevent="
|
||||
if (!submitting) {
|
||||
submitting = true;
|
||||
const formData = new FormData($event.target);
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('manufacturerForm', () => ({
|
||||
submitting: false,
|
||||
|
||||
init() {
|
||||
// Listen for HTMX events on this form
|
||||
this.$el.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.pathInfo.requestPath === '/rides/manufacturers/create/') {
|
||||
this.handleResponse(event);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async submitForm(event) {
|
||||
if (this.submitting) return;
|
||||
|
||||
this.submitting = true;
|
||||
const formData = new FormData(event.target);
|
||||
const csrfToken = this.$el.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
// Use HTMX for form submission
|
||||
htmx.ajax('POST', '/rides/manufacturers/create/', {
|
||||
values: Object.fromEntries(formData),
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
target: this.$el,
|
||||
swap: 'none'
|
||||
});
|
||||
},
|
||||
|
||||
handleResponse(event) {
|
||||
this.submitting = false;
|
||||
|
||||
// Handle HTMX response using event listeners
|
||||
document.addEventListener('htmx:afterRequest', function handleResponse(event) {
|
||||
if (event.detail.pathInfo.requestPath === '/rides/manufacturers/create/') {
|
||||
document.removeEventListener('htmx:afterRequest', handleResponse);
|
||||
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
const data = JSON.parse(event.detail.xhr.response);
|
||||
if (typeof selectManufacturer === 'function') {
|
||||
selectManufacturer(data.id, data.name);
|
||||
}
|
||||
$dispatch('close-manufacturer-modal');
|
||||
}
|
||||
submitting = false;
|
||||
}
|
||||
});
|
||||
}">
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
const data = JSON.parse(event.detail.xhr.response);
|
||||
|
||||
// Dispatch event with manufacturer data for parent components
|
||||
this.$dispatch('manufacturer-created', {
|
||||
id: data.id,
|
||||
name: data.name
|
||||
});
|
||||
|
||||
// Close modal if in modal context
|
||||
this.$dispatch('close-manufacturer-modal');
|
||||
} else {
|
||||
// Handle error case
|
||||
this.$dispatch('manufacturer-creation-error', {
|
||||
error: event.detail.xhr.responseText
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<form method="post"
|
||||
class="space-y-6"
|
||||
x-data="manufacturerForm()"
|
||||
@submit.prevent="submitForm($event)">
|
||||
{% csrf_token %}
|
||||
|
||||
<div id="manufacturer-form-notification"></div>
|
||||
|
||||
@@ -1,53 +1,103 @@
|
||||
{% load static %}
|
||||
|
||||
<form method="post"
|
||||
class="space-y-6"
|
||||
x-data="{
|
||||
submitting: false,
|
||||
manufacturerSearchTerm: '',
|
||||
setManufacturerModal(value, term = '') {
|
||||
const parentForm = document.querySelector('[x-data]');
|
||||
if (parentForm) {
|
||||
const parentData = Alpine.$data(parentForm);
|
||||
if (parentData && parentData.setManufacturerModal) {
|
||||
parentData.setManufacturerModal(value, term);
|
||||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
@submit.prevent="
|
||||
if (!submitting) {
|
||||
submitting = true;
|
||||
const formData = new FormData($event.target);
|
||||
htmx.ajax('POST', '/rides/models/create/', {
|
||||
values: Object.fromEntries(formData),
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('rideModelForm', () => ({
|
||||
submitting: false,
|
||||
manufacturerSearchTerm: '',
|
||||
|
||||
init() {
|
||||
// Listen for HTMX events on this form
|
||||
this.$el.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.pathInfo.requestPath === '/rides/models/create/') {
|
||||
this.handleResponse(event);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle HTMX response using event listeners
|
||||
document.addEventListener('htmx:afterRequest', function handleResponse(event) {
|
||||
if (event.detail.pathInfo.requestPath === '/rides/models/create/') {
|
||||
document.removeEventListener('htmx:afterRequest', handleResponse);
|
||||
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
const data = JSON.parse(event.detail.xhr.response);
|
||||
if (typeof selectRideModel === 'function') {
|
||||
selectRideModel(data.id, data.name);
|
||||
}
|
||||
const parentForm = document.querySelector('[x-data]');
|
||||
if (parentForm) {
|
||||
const parentData = Alpine.$data(parentForm);
|
||||
if (parentData && parentData.setRideModelModal) {
|
||||
parentData.setRideModelModal(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
submitting = false;
|
||||
}
|
||||
// Initialize form with any pre-filled values
|
||||
this.initializeForm();
|
||||
},
|
||||
|
||||
initializeForm() {
|
||||
const searchInput = this.$el.querySelector('#id_ride_model_search');
|
||||
const nameInput = this.$el.querySelector('#id_name');
|
||||
if (searchInput && searchInput.value && nameInput) {
|
||||
nameInput.value = searchInput.value;
|
||||
}
|
||||
},
|
||||
|
||||
setManufacturerModal(value, term = '') {
|
||||
// Dispatch event to parent component to handle manufacturer modal
|
||||
this.$dispatch('set-manufacturer-modal', {
|
||||
show: value,
|
||||
searchTerm: term
|
||||
});
|
||||
}">
|
||||
},
|
||||
|
||||
async submitForm(event) {
|
||||
if (this.submitting) return;
|
||||
|
||||
this.submitting = true;
|
||||
const formData = new FormData(event.target);
|
||||
const csrfToken = this.$el.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
// Use HTMX for form submission
|
||||
htmx.ajax('POST', '/rides/models/create/', {
|
||||
values: Object.fromEntries(formData),
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
target: this.$el,
|
||||
swap: 'none'
|
||||
});
|
||||
},
|
||||
|
||||
handleResponse(event) {
|
||||
this.submitting = false;
|
||||
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
const data = JSON.parse(event.detail.xhr.response);
|
||||
|
||||
// Dispatch event with ride model data for parent components
|
||||
this.$dispatch('ride-model-created', {
|
||||
id: data.id,
|
||||
name: data.name
|
||||
});
|
||||
|
||||
// Close modal if in modal context
|
||||
this.$dispatch('close-ride-model-modal');
|
||||
} else {
|
||||
// Handle error case
|
||||
this.$dispatch('ride-model-creation-error', {
|
||||
error: event.detail.xhr.responseText
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
selectManufacturer(manufacturerId, manufacturerName) {
|
||||
// Update manufacturer fields using AlpineJS reactive approach
|
||||
const manufacturerInput = this.$el.querySelector('#id_manufacturer');
|
||||
const searchInput = this.$el.querySelector('#id_manufacturer_search');
|
||||
const resultsDiv = this.$el.querySelector('#manufacturer-search-results');
|
||||
|
||||
if (manufacturerInput) manufacturerInput.value = manufacturerId;
|
||||
if (searchInput) searchInput.value = manufacturerName;
|
||||
if (resultsDiv) resultsDiv.innerHTML = '';
|
||||
},
|
||||
|
||||
clearManufacturerResults() {
|
||||
const resultsDiv = this.$el.querySelector('#manufacturer-search-results');
|
||||
if (resultsDiv) resultsDiv.innerHTML = '';
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<form method="post"
|
||||
class="space-y-6"
|
||||
x-data="rideModelForm()"
|
||||
@submit.prevent="submitForm($event)"
|
||||
@click.outside="clearManufacturerResults()">
|
||||
{% csrf_token %}
|
||||
|
||||
<div id="ride-model-notification"></div>
|
||||
@@ -175,49 +225,3 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function selectManufacturer(manufacturerId, manufacturerName) {
|
||||
// Update the hidden manufacturer field
|
||||
document.getElementById('id_manufacturer').value = manufacturerId;
|
||||
// Update the search input with the manufacturer name
|
||||
document.getElementById('id_manufacturer_search').value = manufacturerName;
|
||||
// Clear the search results
|
||||
document.getElementById('manufacturer-search-results').innerHTML = '';
|
||||
}
|
||||
|
||||
// Close search results when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
// Get the parent form element that contains the Alpine.js data
|
||||
const formElement = event.target.closest('form[x-data]');
|
||||
if (!formElement) return;
|
||||
|
||||
// Get Alpine.js data from the form
|
||||
const formData = formElement.__x.$data;
|
||||
|
||||
// Don't handle clicks if manufacturer modal is open
|
||||
if (formData.showManufacturerModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchResults = [
|
||||
{ input: 'id_manufacturer_search', results: 'manufacturer-search-results' }
|
||||
];
|
||||
|
||||
searchResults.forEach(function(item) {
|
||||
const input = document.getElementById(item.input);
|
||||
const results = document.getElementById(item.results);
|
||||
if (results && !results.contains(event.target) && event.target !== input) {
|
||||
results.innerHTML = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize form with any pre-filled values
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('id_ride_model_search');
|
||||
if (searchInput && searchInput.value) {
|
||||
document.getElementById('id_name').value = searchInput.value;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
{% block content %}
|
||||
<!-- 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 -->
|
||||
<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 -->
|
||||
<button type="button"
|
||||
id="clear-filters"
|
||||
class="btn-ghost w-full">
|
||||
class="btn-ghost w-full"
|
||||
@click="clearFilters()">
|
||||
<i class="fas fa-times mr-2"></i>
|
||||
Clear All Filters
|
||||
</button>
|
||||
@@ -259,10 +260,10 @@
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -299,22 +300,150 @@
|
||||
</div>
|
||||
|
||||
<!-- AlpineJS Advanced Search Component (HTMX + AlpineJS Only) -->
|
||||
<div x-data="{
|
||||
searchType: 'parks',
|
||||
viewMode: 'grid',
|
||||
toggleSearchType(type) {
|
||||
this.searchType = type;
|
||||
},
|
||||
clearFilters() {
|
||||
document.getElementById('advanced-search-form').reset();
|
||||
this.searchType = 'parks';
|
||||
},
|
||||
setViewMode(mode) {
|
||||
this.viewMode = mode;
|
||||
}
|
||||
}" style="display: none;">
|
||||
<!-- Advanced search functionality handled by AlpineJS + HTMX -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('advancedSearch', () => ({
|
||||
searchType: 'parks',
|
||||
viewMode: 'grid',
|
||||
|
||||
init() {
|
||||
// Initialize range sliders
|
||||
this.updateRangeValues();
|
||||
this.setupRadioButtons();
|
||||
this.setupCheckboxes();
|
||||
},
|
||||
|
||||
toggleSearchType(type) {
|
||||
this.searchType = type;
|
||||
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 -->
|
||||
<style>
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
.status-pending { background: #f59e0b; }
|
||||
</style>
|
||||
</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">
|
||||
<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>
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
<div class="modal-test-container">
|
||||
<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
|
||||
</button>
|
||||
<div class="feature-list">
|
||||
@@ -136,7 +136,7 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
</button>
|
||||
<div class="feature-list">
|
||||
@@ -161,10 +161,10 @@
|
||||
|
||||
<div class="modal-test-container">
|
||||
<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
|
||||
</button>
|
||||
<button class="test-button secondary" onclick="openOriginalModalInMode('register')">
|
||||
<button class="test-button secondary" @click="openOriginalModalInMode('register')">
|
||||
Open in Register Mode
|
||||
</button>
|
||||
<div class="feature-list">
|
||||
@@ -181,10 +181,10 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
</button>
|
||||
<button class="test-button secondary" onclick="openCottonModalInMode('register')">
|
||||
<button class="test-button secondary" @click="openCottonModalInMode('register')">
|
||||
Open in Register Mode
|
||||
</button>
|
||||
<div class="feature-list">
|
||||
@@ -209,7 +209,7 @@
|
||||
|
||||
<div class="modal-test-container">
|
||||
<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
|
||||
</button>
|
||||
<div class="feature-list">
|
||||
@@ -226,7 +226,7 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
</button>
|
||||
<div class="feature-list">
|
||||
@@ -251,7 +251,7 @@
|
||||
|
||||
<div class="modal-test-container">
|
||||
<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
|
||||
</button>
|
||||
<div class="feature-list">
|
||||
@@ -278,7 +278,7 @@
|
||||
|
||||
<div class="modal-test-container">
|
||||
<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
|
||||
</button>
|
||||
<div class="feature-list">
|
||||
@@ -439,73 +439,89 @@
|
||||
}));
|
||||
});
|
||||
|
||||
// Store references to both modal instances
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Wait for Alpine.js to initialize and modal instances to be created
|
||||
setTimeout(() => {
|
||||
// Both modals should now be available with their respective window keys
|
||||
console.log('Auth Modal References:', {
|
||||
original: window.authModalOriginal,
|
||||
cotton: window.authModalCotton,
|
||||
custom: window.authModalCustom
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
// Auth Modal Test Suite Component
|
||||
Alpine.data('authModalTestSuite', () => ({
|
||||
init() {
|
||||
// Wait for Alpine.js to initialize and modal instances to be created
|
||||
setTimeout(() => {
|
||||
console.log('Auth Modal References:', {
|
||||
original: window.authModalOriginal,
|
||||
cotton: window.authModalCotton,
|
||||
custom: window.authModalCustom
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
|
||||
// Test functions
|
||||
function openOriginalModalInMode(mode) {
|
||||
if (window.authModalOriginal) {
|
||||
window.authModalOriginal.mode = mode;
|
||||
window.authModalOriginal.open = true;
|
||||
}
|
||||
}
|
||||
openOriginalModal() {
|
||||
if (window.authModalOriginal) {
|
||||
window.authModalOriginal.open = true;
|
||||
}
|
||||
},
|
||||
|
||||
function openCottonModalInMode(mode) {
|
||||
if (window.authModalCotton) {
|
||||
window.authModalCotton.mode = mode;
|
||||
window.authModalCotton.open = true;
|
||||
}
|
||||
}
|
||||
openCottonModal() {
|
||||
if (window.authModalCotton) {
|
||||
window.authModalCotton.open = true;
|
||||
}
|
||||
},
|
||||
|
||||
function testOriginalInteractivity() {
|
||||
if (window.authModalOriginal) {
|
||||
window.authModalOriginal.open = true;
|
||||
window.authModalOriginal.mode = 'login';
|
||||
setTimeout(() => {
|
||||
window.authModalOriginal.loginError = 'Test error message';
|
||||
window.authModalOriginal.showPassword = true;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
openOriginalModalInMode(mode) {
|
||||
if (window.authModalOriginal) {
|
||||
window.authModalOriginal.mode = mode;
|
||||
window.authModalOriginal.open = true;
|
||||
}
|
||||
},
|
||||
|
||||
function testCottonInteractivity() {
|
||||
if (window.authModalCotton) {
|
||||
window.authModalCotton.open = true;
|
||||
window.authModalCotton.mode = 'login';
|
||||
setTimeout(() => {
|
||||
window.authModalCotton.loginError = 'Test error message';
|
||||
window.authModalCotton.showPassword = true;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
openCottonModalInMode(mode) {
|
||||
if (window.authModalCotton) {
|
||||
window.authModalCotton.mode = mode;
|
||||
window.authModalCotton.open = true;
|
||||
}
|
||||
},
|
||||
|
||||
function compareModalStyling() {
|
||||
if (window.authModalOriginal && window.authModalCotton) {
|
||||
window.authModalOriginal.open = true;
|
||||
setTimeout(() => {
|
||||
window.authModalCotton.open = true;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
testOriginalInteractivity() {
|
||||
if (window.authModalOriginal) {
|
||||
window.authModalOriginal.open = true;
|
||||
window.authModalOriginal.mode = 'login';
|
||||
setTimeout(() => {
|
||||
window.authModalOriginal.loginError = 'Test error message';
|
||||
window.authModalOriginal.showPassword = true;
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
|
||||
function testCustomConfiguration() {
|
||||
// Show the custom cotton modal
|
||||
const customModal = document.getElementById('custom-cotton-modal');
|
||||
customModal.style.display = 'block';
|
||||
|
||||
// You would implement custom Alpine.js instance here
|
||||
alert('Custom configuration test - check the modal titles and text changes');
|
||||
}
|
||||
testCottonInteractivity() {
|
||||
if (window.authModalCotton) {
|
||||
window.authModalCotton.open = true;
|
||||
window.authModalCotton.mode = 'login';
|
||||
setTimeout(() => {
|
||||
window.authModalCotton.loginError = 'Test error message';
|
||||
window.authModalCotton.showPassword = true;
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
|
||||
compareModalStyling() {
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
</style>
|
||||
</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">
|
||||
<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>
|
||||
@@ -582,73 +582,95 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alpine.js -->
|
||||
<script src="{% static 'js/alpine.min.js' %}" defer></script>
|
||||
|
||||
<script>
|
||||
// Function to normalize HTML for comparison
|
||||
function normalizeHTML(html) {
|
||||
return html
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/> </g, '><')
|
||||
.trim();
|
||||
}
|
||||
document.addEventListener('alpine:init', () => {
|
||||
// Component Test Suite Component
|
||||
Alpine.data('componentTestSuite', () => ({
|
||||
init() {
|
||||
// Extract HTML after Alpine.js initializes
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => this.extractComponentHTML(), 100);
|
||||
this.addCompareButton();
|
||||
});
|
||||
},
|
||||
|
||||
// Function to extract HTML from all component containers
|
||||
function extractComponentHTML() {
|
||||
const containers = document.querySelectorAll('.button-container');
|
||||
const includeHTMLs = [];
|
||||
const cottonHTMLs = [];
|
||||
let componentIndex = 1;
|
||||
// Function to normalize HTML for comparison
|
||||
normalizeHTML(html) {
|
||||
return html
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/> </g, '><')
|
||||
.trim();
|
||||
},
|
||||
|
||||
containers.forEach((container, index) => {
|
||||
const label = container.getAttribute('data-label');
|
||||
// Look for button, input, or div (card) elements
|
||||
const element = container.querySelector('button') ||
|
||||
container.querySelector('input') ||
|
||||
container.querySelector('div.rounded-lg');
|
||||
|
||||
if (element && label) {
|
||||
const html = element.outerHTML;
|
||||
const normalized = normalizeHTML(html);
|
||||
// Function to extract HTML from all component containers
|
||||
extractComponentHTML() {
|
||||
const containers = this.$el.querySelectorAll('.button-container');
|
||||
const includeHTMLs = [];
|
||||
const cottonHTMLs = [];
|
||||
let componentIndex = 1;
|
||||
|
||||
containers.forEach((container, index) => {
|
||||
const label = container.getAttribute('data-label');
|
||||
// Look for button, input, or div (card) elements
|
||||
const element = container.querySelector('button') ||
|
||||
container.querySelector('input') ||
|
||||
container.querySelector('div.rounded-lg');
|
||||
|
||||
if (element && label) {
|
||||
const html = element.outerHTML;
|
||||
const normalized = this.normalizeHTML(html);
|
||||
|
||||
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 (label === 'Include Version') {
|
||||
includeHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
|
||||
} else if (label === 'Cotton Version') {
|
||||
cottonHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
|
||||
componentIndex++;
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user