Compare commits

..

5 Commits

10 changed files with 726 additions and 436 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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