Compare commits

...

5 Commits

10 changed files with 726 additions and 436 deletions

View File

@@ -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!!!

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>