mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-31 13:07:04 -05:00
Compare commits
5 Commits
5b7b203619
...
c437ddbf28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c437ddbf28 | ||
|
|
f7b1296263 | ||
|
|
e53414d795 | ||
|
|
2328c919c9 | ||
|
|
09e2c69493 |
@@ -50,3 +50,5 @@ tailwindcss, django, django-cotton, htmx, alpinejs, django-rest-framework, postg
|
|||||||
- Real database data only (NO MOCKING)
|
- Real database data only (NO MOCKING)
|
||||||
- RichChoiceField over Django choices
|
- 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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">
|
<div id="dashboard-content" class="relative transition-all duration-200">
|
||||||
{% block moderation_content %}
|
{% block moderation_content %}
|
||||||
{% include "moderation/partials/dashboard_content.html" %}
|
{% include "moderation/partials/dashboard_content.html" %}
|
||||||
@@ -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,79 +180,91 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<!-- AlpineJS Moderation Dashboard Component (HTMX + AlpineJS Only) -->
|
<script>
|
||||||
<div x-data="{
|
document.addEventListener('alpine:init', () => {
|
||||||
|
// Moderation Dashboard Component
|
||||||
|
Alpine.data('moderationDashboard', () => ({
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
errorMessage: null,
|
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>
|
init() {
|
||||||
// HTMX Configuration
|
// HTMX Configuration
|
||||||
document.body.addEventListener('htmx:configRequest', function(evt) {
|
this.setupHTMXConfig();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.setupSearchDebouncing();
|
||||||
|
this.setupInfiniteScroll();
|
||||||
|
this.setupKeyboardNavigation();
|
||||||
|
},
|
||||||
|
|
||||||
|
setupHTMXConfig() {
|
||||||
|
document.body.addEventListener('htmx:configRequest', (evt) => {
|
||||||
evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Loading and Error State Management
|
|
||||||
const dashboard = {
|
|
||||||
content: document.getElementById('dashboard-content'),
|
|
||||||
skeleton: document.getElementById('loading-skeleton'),
|
|
||||||
errorState: document.getElementById('error-state'),
|
|
||||||
errorMessage: document.getElementById('error-message'),
|
|
||||||
|
|
||||||
showLoading() {
|
|
||||||
this.content.setAttribute('aria-busy', 'true');
|
|
||||||
this.content.style.opacity = '0';
|
|
||||||
this.errorState.classList.add('hidden');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hideLoading() {
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('htmx:afterOnLoad', (evt) => {
|
||||||
|
if (evt.detail.target.id === 'dashboard-content') {
|
||||||
|
this.hideLoadingState();
|
||||||
|
this.resetFocus(evt.detail.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('htmx:responseError', (evt) => {
|
||||||
|
if (evt.detail.target.id === 'dashboard-content') {
|
||||||
|
this.showErrorState(evt.detail.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
showError(message) {
|
showLoadingState() {
|
||||||
this.errorState.classList.remove('hidden');
|
const content = this.$el.querySelector('#dashboard-content');
|
||||||
this.errorMessage.textContent = message || 'There was a problem loading the content. Please try again.';
|
if (content) {
|
||||||
// Announce error to screen readers
|
content.setAttribute('aria-busy', 'true');
|
||||||
this.errorMessage.setAttribute('role', 'alert');
|
content.style.opacity = '0';
|
||||||
}
|
}
|
||||||
};
|
const errorState = this.$el.querySelector('#error-state');
|
||||||
|
if (errorState) {
|
||||||
// Enhanced HTMX Event Handlers
|
errorState.classList.add('hidden');
|
||||||
document.body.addEventListener('htmx:beforeRequest', function(evt) {
|
|
||||||
if (evt.detail.target.id === 'dashboard-content') {
|
|
||||||
dashboard.showLoading();
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
|
||||||
document.body.addEventListener('htmx:afterOnLoad', function(evt) {
|
hideLoadingState() {
|
||||||
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', 'false');
|
||||||
const firstFocusable = evt.detail.target.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
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) {
|
if (firstFocusable) {
|
||||||
firstFocusable.focus();
|
firstFocusable.focus();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
|
||||||
|
|
||||||
document.body.addEventListener('htmx:responseError', function(evt) {
|
debounce(func, wait) {
|
||||||
if (evt.detail.target.id === 'dashboard-content') {
|
|
||||||
dashboard.showError(evt.detail.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Search Input Debouncing
|
|
||||||
function debounce(func, wait) {
|
|
||||||
let timeout;
|
let timeout;
|
||||||
return function executedFunction(...args) {
|
return function executedFunction(...args) {
|
||||||
const later = () => {
|
const later = () => {
|
||||||
@@ -262,51 +274,62 @@ function debounce(func, wait) {
|
|||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(later, wait);
|
timeout = setTimeout(later, wait);
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
// Apply debouncing to search inputs
|
setupSearchDebouncing() {
|
||||||
document.querySelectorAll('[data-search]').forEach(input => {
|
const searchInputs = this.$el.querySelectorAll('[data-search]');
|
||||||
|
searchInputs.forEach(input => {
|
||||||
const originalSearch = () => {
|
const originalSearch = () => {
|
||||||
htmx.trigger(input, 'input');
|
htmx.trigger(input, 'input');
|
||||||
};
|
};
|
||||||
const debouncedSearch = debounce(originalSearch, 300);
|
const debouncedSearch = this.debounce(originalSearch, 300);
|
||||||
|
|
||||||
input.addEventListener('input', (e) => {
|
input.addEventListener('input', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
debouncedSearch();
|
debouncedSearch();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Virtual Scrolling for Large Lists
|
setupInfiniteScroll() {
|
||||||
const observerOptions = {
|
const observerOptions = {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '100px',
|
rootMargin: '100px',
|
||||||
threshold: 0.1
|
threshold: 0.1
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMoreContent = (entries, observer) => {
|
const loadMoreContent = (entries, observer) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting && !entry.target.classList.contains('loading')) {
|
if (entry.isIntersecting && !entry.target.classList.contains('loading')) {
|
||||||
entry.target.classList.add('loading');
|
entry.target.classList.add('loading');
|
||||||
htmx.trigger(entry.target, 'intersect');
|
htmx.trigger(entry.target, 'intersect');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const observer = new IntersectionObserver(loadMoreContent, observerOptions);
|
const observer = new IntersectionObserver(loadMoreContent, observerOptions);
|
||||||
document.querySelectorAll('[data-infinite-scroll]').forEach(el => observer.observe(el));
|
const infiniteScrollElements = this.$el.querySelectorAll('[data-infinite-scroll]');
|
||||||
|
infiniteScrollElements.forEach(el => observer.observe(el));
|
||||||
|
},
|
||||||
|
|
||||||
// Keyboard Navigation Enhancement
|
setupKeyboardNavigation() {
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
const openModals = document.querySelectorAll('[x-show="showNotes"]');
|
const openModals = this.$el.querySelectorAll('[x-show="showNotes"]');
|
||||||
openModals.forEach(modal => {
|
openModals.forEach(modal => {
|
||||||
const alpineData = modal.__x.$data;
|
const alpineData = modal.__x?.$data;
|
||||||
if (alpineData.showNotes) {
|
if (alpineData && alpineData.showNotes) {
|
||||||
alpineData.showNotes = false;
|
alpineData.showNotes = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
retryLoad() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% 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 %}
|
{% if parks %}
|
||||||
{% for park in parks %}
|
{% for park in parks %}
|
||||||
<button type="button"
|
<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"
|
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 }}
|
{{ park.name }}
|
||||||
</button>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -17,11 +40,3 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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,20 +173,37 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Search Suggestions -->
|
<!-- 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>
|
<span class="text-xs text-muted-foreground">Try:</span>
|
||||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors"
|
<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
|
Disney
|
||||||
</button>
|
</button>
|
||||||
<span class="text-xs text-muted-foreground">•</span>
|
<span class="text-xs text-muted-foreground">•</span>
|
||||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors"
|
<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
|
Roller Coaster
|
||||||
</button>
|
</button>
|
||||||
<span class="text-xs text-muted-foreground">•</span>
|
<span class="text-xs text-muted-foreground">•</span>
|
||||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors"
|
<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
|
Cedar Point
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,35 +1,66 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<form method="post"
|
<script>
|
||||||
class="space-y-6"
|
document.addEventListener('alpine:init', () => {
|
||||||
x-data="{ submitting: false }"
|
Alpine.data('designerForm', () => ({
|
||||||
@submit.prevent="
|
submitting: false,
|
||||||
if (!submitting) {
|
|
||||||
submitting = true;
|
init() {
|
||||||
const formData = new FormData($event.target);
|
// 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/', {
|
htmx.ajax('POST', '/rides/designers/create/', {
|
||||||
values: Object.fromEntries(formData),
|
values: Object.fromEntries(formData),
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
'X-CSRFToken': csrfToken
|
||||||
}
|
},
|
||||||
|
target: this.$el,
|
||||||
|
swap: 'none'
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Handle HTMX response using event listeners
|
handleResponse(event) {
|
||||||
document.addEventListener('htmx:afterRequest', function handleResponse(event) {
|
this.submitting = false;
|
||||||
if (event.detail.pathInfo.requestPath === '/rides/designers/create/') {
|
|
||||||
document.removeEventListener('htmx:afterRequest', handleResponse);
|
|
||||||
|
|
||||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||||
const data = JSON.parse(event.detail.xhr.response);
|
const data = JSON.parse(event.detail.xhr.response);
|
||||||
if (typeof selectDesigner === 'function') {
|
|
||||||
selectDesigner(data.id, data.name);
|
// Dispatch event with designer data for parent components
|
||||||
}
|
this.$dispatch('designer-created', {
|
||||||
$dispatch('close-designer-modal');
|
id: data.id,
|
||||||
}
|
name: data.name
|
||||||
submitting = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}">
|
|
||||||
|
// 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 %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div id="designer-form-notification"></div>
|
<div id="designer-form-notification"></div>
|
||||||
|
|||||||
@@ -1,35 +1,66 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<form method="post"
|
<script>
|
||||||
class="space-y-6"
|
document.addEventListener('alpine:init', () => {
|
||||||
x-data="{ submitting: false }"
|
Alpine.data('manufacturerForm', () => ({
|
||||||
@submit.prevent="
|
submitting: false,
|
||||||
if (!submitting) {
|
|
||||||
submitting = true;
|
init() {
|
||||||
const formData = new FormData($event.target);
|
// 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/', {
|
htmx.ajax('POST', '/rides/manufacturers/create/', {
|
||||||
values: Object.fromEntries(formData),
|
values: Object.fromEntries(formData),
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
'X-CSRFToken': csrfToken
|
||||||
}
|
},
|
||||||
|
target: this.$el,
|
||||||
|
swap: 'none'
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Handle HTMX response using event listeners
|
handleResponse(event) {
|
||||||
document.addEventListener('htmx:afterRequest', function handleResponse(event) {
|
this.submitting = false;
|
||||||
if (event.detail.pathInfo.requestPath === '/rides/manufacturers/create/') {
|
|
||||||
document.removeEventListener('htmx:afterRequest', handleResponse);
|
|
||||||
|
|
||||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||||
const data = JSON.parse(event.detail.xhr.response);
|
const data = JSON.parse(event.detail.xhr.response);
|
||||||
if (typeof selectManufacturer === 'function') {
|
|
||||||
selectManufacturer(data.id, data.name);
|
// Dispatch event with manufacturer data for parent components
|
||||||
}
|
this.$dispatch('manufacturer-created', {
|
||||||
$dispatch('close-manufacturer-modal');
|
id: data.id,
|
||||||
}
|
name: data.name
|
||||||
submitting = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}">
|
|
||||||
|
// 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 %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div id="manufacturer-form-notification"></div>
|
<div id="manufacturer-form-notification"></div>
|
||||||
|
|||||||
@@ -1,53 +1,103 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<form method="post"
|
<script>
|
||||||
class="space-y-6"
|
document.addEventListener('alpine:init', () => {
|
||||||
x-data="{
|
Alpine.data('rideModelForm', () => ({
|
||||||
submitting: false,
|
submitting: false,
|
||||||
manufacturerSearchTerm: '',
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 = '') {
|
setManufacturerModal(value, term = '') {
|
||||||
const parentForm = document.querySelector('[x-data]');
|
// Dispatch event to parent component to handle manufacturer modal
|
||||||
if (parentForm) {
|
this.$dispatch('set-manufacturer-modal', {
|
||||||
const parentData = Alpine.$data(parentForm);
|
show: value,
|
||||||
if (parentData && parentData.setManufacturerModal) {
|
searchTerm: term
|
||||||
parentData.setManufacturerModal(value, term);
|
});
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
async submitForm(event) {
|
||||||
}"
|
if (this.submitting) return;
|
||||||
@submit.prevent="
|
|
||||||
if (!submitting) {
|
this.submitting = true;
|
||||||
submitting = true;
|
const formData = new FormData(event.target);
|
||||||
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/', {
|
htmx.ajax('POST', '/rides/models/create/', {
|
||||||
values: Object.fromEntries(formData),
|
values: Object.fromEntries(formData),
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
'X-CSRFToken': csrfToken
|
||||||
}
|
},
|
||||||
|
target: this.$el,
|
||||||
|
swap: 'none'
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Handle HTMX response using event listeners
|
handleResponse(event) {
|
||||||
document.addEventListener('htmx:afterRequest', function handleResponse(event) {
|
this.submitting = false;
|
||||||
if (event.detail.pathInfo.requestPath === '/rides/models/create/') {
|
|
||||||
document.removeEventListener('htmx:afterRequest', handleResponse);
|
|
||||||
|
|
||||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||||
const data = JSON.parse(event.detail.xhr.response);
|
const data = JSON.parse(event.detail.xhr.response);
|
||||||
if (typeof selectRideModel === 'function') {
|
|
||||||
selectRideModel(data.id, data.name);
|
// Dispatch event with ride model data for parent components
|
||||||
}
|
this.$dispatch('ride-model-created', {
|
||||||
const parentForm = document.querySelector('[x-data]');
|
id: data.id,
|
||||||
if (parentForm) {
|
name: data.name
|
||||||
const parentData = Alpine.$data(parentForm);
|
|
||||||
if (parentData && parentData.setRideModelModal) {
|
|
||||||
parentData.setRideModelModal(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
submitting = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}">
|
|
||||||
|
// 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 %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div id="ride-model-notification"></div>
|
<div id="ride-model-notification"></div>
|
||||||
@@ -175,49 +225,3 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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 %}
|
{% 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>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('advancedSearch', () => ({
|
||||||
searchType: 'parks',
|
searchType: 'parks',
|
||||||
viewMode: 'grid',
|
viewMode: 'grid',
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Initialize range sliders
|
||||||
|
this.updateRangeValues();
|
||||||
|
this.setupRadioButtons();
|
||||||
|
this.setupCheckboxes();
|
||||||
|
},
|
||||||
|
|
||||||
toggleSearchType(type) {
|
toggleSearchType(type) {
|
||||||
this.searchType = 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() {
|
clearFilters() {
|
||||||
document.getElementById('advanced-search-form').reset();
|
const form = this.$el.querySelector('#advanced-search-form');
|
||||||
|
if (form) {
|
||||||
|
form.reset();
|
||||||
this.searchType = 'parks';
|
this.searchType = 'parks';
|
||||||
|
this.toggleSearchType('parks');
|
||||||
|
this.updateRangeValues();
|
||||||
|
this.setupRadioButtons();
|
||||||
|
this.setupCheckboxes();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setViewMode(mode) {
|
setViewMode(mode) {
|
||||||
this.viewMode = 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');
|
||||||
}
|
}
|
||||||
}" style="display: none;">
|
},
|
||||||
<!-- Advanced search functionality handled by AlpineJS + HTMX -->
|
|
||||||
</div>
|
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,35 +439,46 @@
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store references to both modal instances
|
// Auth Modal Test Suite Component
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
Alpine.data('authModalTestSuite', () => ({
|
||||||
|
init() {
|
||||||
// Wait for Alpine.js to initialize and modal instances to be created
|
// Wait for Alpine.js to initialize and modal instances to be created
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Both modals should now be available with their respective window keys
|
|
||||||
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) {
|
||||||
|
window.authModalOriginal.open = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openCottonModal() {
|
||||||
|
if (window.authModalCotton) {
|
||||||
|
window.authModalCotton.open = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openOriginalModalInMode(mode) {
|
||||||
if (window.authModalOriginal) {
|
if (window.authModalOriginal) {
|
||||||
window.authModalOriginal.mode = mode;
|
window.authModalOriginal.mode = mode;
|
||||||
window.authModalOriginal.open = true;
|
window.authModalOriginal.open = true;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
function openCottonModalInMode(mode) {
|
openCottonModalInMode(mode) {
|
||||||
if (window.authModalCotton) {
|
if (window.authModalCotton) {
|
||||||
window.authModalCotton.mode = mode;
|
window.authModalCotton.mode = mode;
|
||||||
window.authModalCotton.open = true;
|
window.authModalCotton.open = true;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
function testOriginalInteractivity() {
|
testOriginalInteractivity() {
|
||||||
if (window.authModalOriginal) {
|
if (window.authModalOriginal) {
|
||||||
window.authModalOriginal.open = true;
|
window.authModalOriginal.open = true;
|
||||||
window.authModalOriginal.mode = 'login';
|
window.authModalOriginal.mode = 'login';
|
||||||
@@ -476,9 +487,9 @@
|
|||||||
window.authModalOriginal.showPassword = true;
|
window.authModalOriginal.showPassword = true;
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
function testCottonInteractivity() {
|
testCottonInteractivity() {
|
||||||
if (window.authModalCotton) {
|
if (window.authModalCotton) {
|
||||||
window.authModalCotton.open = true;
|
window.authModalCotton.open = true;
|
||||||
window.authModalCotton.mode = 'login';
|
window.authModalCotton.mode = 'login';
|
||||||
@@ -487,25 +498,30 @@
|
|||||||
window.authModalCotton.showPassword = true;
|
window.authModalCotton.showPassword = true;
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
function compareModalStyling() {
|
compareModalStyling() {
|
||||||
if (window.authModalOriginal && window.authModalCotton) {
|
if (window.authModalOriginal && window.authModalCotton) {
|
||||||
window.authModalOriginal.open = true;
|
window.authModalOriginal.open = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.authModalCotton.open = true;
|
window.authModalCotton.open = true;
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
function testCustomConfiguration() {
|
testCustomConfiguration() {
|
||||||
// Show the custom cotton modal
|
// Show the custom cotton modal
|
||||||
const customModal = document.getElementById('custom-cotton-modal');
|
const customModal = this.$el.querySelector('#custom-cotton-modal');
|
||||||
|
if (customModal) {
|
||||||
customModal.style.display = 'block';
|
customModal.style.display = 'block';
|
||||||
|
|
||||||
// You would implement custom Alpine.js instance here
|
|
||||||
alert('Custom configuration test - check the modal titles and text changes');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,18 +582,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Alpine.js -->
|
||||||
|
<script src="{% static 'js/alpine.min.js' %}" defer></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
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 normalize HTML for comparison
|
// Function to normalize HTML for comparison
|
||||||
function normalizeHTML(html) {
|
normalizeHTML(html) {
|
||||||
return html
|
return html
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.replace(/> </g, '><')
|
.replace(/> </g, '><')
|
||||||
.trim();
|
.trim();
|
||||||
}
|
},
|
||||||
|
|
||||||
// Function to extract HTML from all component containers
|
// Function to extract HTML from all component containers
|
||||||
function extractComponentHTML() {
|
extractComponentHTML() {
|
||||||
const containers = document.querySelectorAll('.button-container');
|
const containers = this.$el.querySelectorAll('.button-container');
|
||||||
const includeHTMLs = [];
|
const includeHTMLs = [];
|
||||||
const cottonHTMLs = [];
|
const cottonHTMLs = [];
|
||||||
let componentIndex = 1;
|
let componentIndex = 1;
|
||||||
@@ -607,7 +621,7 @@
|
|||||||
|
|
||||||
if (element && label) {
|
if (element && label) {
|
||||||
const html = element.outerHTML;
|
const html = element.outerHTML;
|
||||||
const normalized = normalizeHTML(html);
|
const normalized = this.normalizeHTML(html);
|
||||||
|
|
||||||
if (label === 'Include Version') {
|
if (label === 'Include Version') {
|
||||||
includeHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
|
includeHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
|
||||||
@@ -618,36 +632,44 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('include-html').textContent = includeHTMLs.join('\n');
|
const includeElement = this.$el.querySelector('#include-html');
|
||||||
document.getElementById('cotton-html').textContent = cottonHTMLs.join('\n');
|
const cottonElement = this.$el.querySelector('#cotton-html');
|
||||||
}
|
|
||||||
|
|
||||||
// Extract HTML after page loads
|
if (includeElement) includeElement.textContent = includeHTMLs.join('\n');
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
if (cottonElement) cottonElement.textContent = cottonHTMLs.join('\n');
|
||||||
setTimeout(extractComponentHTML, 100);
|
},
|
||||||
});
|
|
||||||
|
|
||||||
// Function to compare HTML outputs
|
// Function to compare HTML outputs
|
||||||
function compareHTML() {
|
compareHTML() {
|
||||||
const includeHTML = document.getElementById('include-html').textContent;
|
const includeHTML = this.$el.querySelector('#include-html')?.textContent || '';
|
||||||
const cottonHTML = document.getElementById('cotton-html').textContent;
|
const cottonHTML = this.$el.querySelector('#cotton-html')?.textContent || '';
|
||||||
|
|
||||||
if (includeHTML === cottonHTML) {
|
if (includeHTML === cottonHTML) {
|
||||||
alert('✅ HTML outputs are identical!');
|
this.$dispatch('comparison-result', {
|
||||||
|
success: true,
|
||||||
|
message: '✅ HTML outputs are identical!'
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert('❌ HTML outputs differ. Check the HTML Output section for details.');
|
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('Include HTML:', includeHTML);
|
||||||
console.log('Cotton HTML:', cottonHTML);
|
console.log('Cotton HTML:', cottonHTML);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
// Add compare button
|
// Add compare button
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
addCompareButton() {
|
||||||
const compareBtn = document.createElement('button');
|
const compareBtn = document.createElement('button');
|
||||||
compareBtn.textContent = 'Compare HTML Outputs';
|
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.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;
|
compareBtn.addEventListener('click', () => this.compareHTML());
|
||||||
document.body.appendChild(compareBtn);
|
document.body.appendChild(compareBtn);
|
||||||
|
}
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user