Enhance moderation dashboard UI and UX:

- Add HTMX-powered filtering with instant updates
- Add smooth transitions and loading states
- Improve visual hierarchy and styling
- Add review notes functionality
- Add confirmation dialogs for actions
- Make navigation sticky
- Add hover effects and visual feedback
- Improve dark mode support
This commit is contained in:
pacnpal
2024-11-13 14:38:38 +00:00
parent d2c9d02523
commit 9ee380c3ea
98 changed files with 5073 additions and 3040 deletions

View File

@@ -0,0 +1,47 @@
<!-- Add Ride Modal -->
<div id="add-ride-modal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen p-4">
<!-- Background overlay -->
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" aria-hidden="true"></div>
<!-- Modal panel -->
<div class="relative w-full max-w-3xl p-6 mx-auto bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
Add Ride at {{ park.name }}
</h2>
</div>
<div id="modal-content">
{% include "rides/partials/ride_form.html" with modal=True %}
</div>
</div>
</div>
</div>
<!-- Modal Toggle Button -->
<button type="button"
onclick="openModal('add-ride-modal')"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Add Ride
</button>
<script>
function openModal(modalId) {
document.getElementById(modalId).classList.remove('hidden');
}
function closeModal() {
document.getElementById('add-ride-modal').classList.add('hidden');
}
// Close modal when clicking outside
document.getElementById('add-ride-modal').addEventListener('click', function(event) {
if (event.target === this) {
closeModal();
}
});
</script>

View File

@@ -0,0 +1,110 @@
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Left Column -->
<div class="space-y-6">
<div>
<label for="id_height_ft" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Height (ft)
</label>
<input type="number"
name="height_ft"
id="id_height_ft"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Total height of the coaster in feet"
min="0">
</div>
<div>
<label for="id_length_ft" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Length (ft)
</label>
<input type="number"
name="length_ft"
id="id_length_ft"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Total track length in feet"
min="0">
</div>
<div>
<label for="id_speed_mph" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Speed (mph)
</label>
<input type="number"
name="speed_mph"
id="id_speed_mph"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Maximum speed in miles per hour"
min="0">
</div>
<div>
<label for="id_inversions" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Inversions
</label>
<input type="number"
name="inversions"
id="id_inversions"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Number of inversions"
min="0">
</div>
</div>
<!-- Right Column -->
<div class="space-y-6">
<div>
<label for="id_track_material" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Track Material
</label>
<select name="track_material"
id="id_track_material"
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">Select track material...</option>
<option value="STEEL">Steel</option>
<option value="WOOD">Wood</option>
<option value="HYBRID">Hybrid</option>
<option value="OTHER">Other</option>
</select>
</div>
<div>
<label for="id_roller_coaster_type" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Coaster Type
</label>
<select name="roller_coaster_type"
id="id_roller_coaster_type"
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">Select coaster type...</option>
<option value="SITDOWN">Sit-Down</option>
<option value="INVERTED">Inverted</option>
<option value="FLYING">Flying</option>
<option value="STANDUP">Stand-Up</option>
<option value="WING">Wing</option>
<option value="SUSPENDED">Suspended</option>
<option value="BOBSLED">Bobsled</option>
<option value="PIPELINE">Pipeline</option>
<option value="MOTORBIKE">Motorbike</option>
<option value="FLOORLESS">Floorless</option>
<option value="DIVE">Dive</option>
<option value="FAMILY">Family</option>
<option value="WILD_MOUSE">Wild Mouse</option>
<option value="SPINNING">Spinning</option>
<option value="FOURTH_DIMENSION">4th Dimension</option>
<option value="OTHER">Other</option>
</select>
</div>
<div>
<label for="id_launch_type" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Launch Type
</label>
<select name="launch_type"
id="id_launch_type"
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">Select launch type...</option>
<option value="CHAIN">Chain Lift</option>
<option value="CABLE">Cable Launch</option>
<option value="HYDRAULIC">Hydraulic Launch</option>
<option value="LSM">Linear Synchronous Motor</option>
<option value="LIM">Linear Induction Motor</option>
<option value="GRAVITY">Gravity</option>
<option value="OTHER">Other</option>
</select>
</div>
</div>
</div>

View File

@@ -0,0 +1,93 @@
{% load static %}
<div class="bg-white rounded-lg shadow-xl modal-content dark:bg-gray-800"
x-data="{ showModal: true }"
@click.outside="$dispatch('close-modal')"
@keydown.escape.window="$dispatch('close-modal')">
<div class="p-6">
<h2 class="mb-4 text-xl font-semibold dark:text-white">Create New Ride Model</h2>
<form hx-post="{% url 'rides:create_ride_model' %}"
hx-target="#modal-content"
class="space-y-4">
{% csrf_token %}
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name *</label>
<input type="text"
id="name"
name="name"
value="{{ name }}"
required
class="w-full mt-1 border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white">
</div>
<div>
<label for="manufacturer" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Manufacturer</label>
<select id="manufacturer"
name="manufacturer"
class="w-full mt-1 border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">Select a manufacturer</option>
{% for manufacturer in manufacturers %}
<option value="{{ manufacturer.id }}">{{ manufacturer.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Category *</label>
<select id="category"
name="category"
required
class="w-full mt-1 border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">Select a category</option>
{% for code, name in categories %}
<option value="{{ code }}">{{ name }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
<textarea id="description"
name="description"
rows="3"
class="w-full mt-1 border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Enter a description of this ride model"></textarea>
</div>
{% if not user.is_privileged %}
<div>
<label for="reason" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Reason for Addition *</label>
<textarea id="reason"
name="reason"
rows="2"
required
class="w-full mt-1 border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Why are you adding this ride model?"></textarea>
</div>
<div>
<label for="source" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Source</label>
<input type="text"
id="source"
name="source"
class="w-full mt-1 border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="URL or reference for this information">
</div>
{% endif %}
<div class="flex justify-end mt-6 space-x-3">
<button type="button"
@click="$dispatch('close-modal')"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600">
Cancel
</button>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 dark:hover:bg-blue-500">
Create Ride Model
</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,26 @@
{% if error %}
<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
{{ error }}
</div>
{% else %}
<div class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert">
{% if designer.id %}
Designer "{{ designer.name }}" has been created successfully.
<script>
// Update the designer field in the parent form
selectDesigner('{{ designer.id }}', '{{ designer.name }}');
// Close the modal
document.dispatchEvent(new CustomEvent('close-designer-modal'));
</script>
{% else %}
Your designer submission "{{ designer.name }}" has been sent for review.
You will be notified when it is approved.
<script>
// Close the modal after a short delay
setTimeout(() => {
document.dispatchEvent(new CustomEvent('close-designer-modal'));
}, 2000);
</script>
{% endif %}
</div>
{% endif %}

View File

@@ -0,0 +1,84 @@
{% 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);
htmx.ajax('POST', '/rides/designers/create/', {
values: Object.fromEntries(formData),
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
}).then(response => {
if (response.detail) {
const data = JSON.parse(response.detail.xhr.response);
selectDesigner(data.id, data.name);
}
$dispatch('close-designer-modal');
}).finally(() => {
submitting = false;
});
}">
{% csrf_token %}
<div id="designer-form-notification"></div>
<div class="space-y-6">
<!-- Name -->
<div>
<label for="designer_name" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name *
</label>
<input type="text"
name="name"
id="designer_name"
value="{{ search_term|default:'' }}"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
required>
</div>
{% if not user.is_privileged %}
<!-- Reason and Source for non-privileged users -->
<div>
<label for="reason" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Reason for Addition *
</label>
<textarea name="reason"
id="reason"
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="3"
required
placeholder="Please explain why you're adding this designer and provide any relevant details."></textarea>
</div>
<div>
<label for="source" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Source (Optional)
</label>
<input type="text"
name="source"
id="source"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="URL or reference for this information">
</div>
{% endif %}
</div>
<div class="flex justify-end mt-6 space-x-3">
{% if modal %}
<button type="button"
@click="$dispatch('close-designer-modal')"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600">
Cancel
</button>
{% endif %}
<button type="submit"
:disabled="submitting"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 dark:hover:bg-blue-500 disabled:opacity-50">
<span x-show="!submitting">Create Designer</span>
<span x-show="submitting">Creating...</span>
</button>
</div>
</form>

View File

@@ -0,0 +1,27 @@
<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;">
{% if designers %}
{% for designer in designers %}
<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="selectDesigner('{{ designer.id }}', '{{ designer.name|escapejs }}')">
{{ designer.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No matches found. You can still submit this name.
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectDesigner(id, name) {
document.getElementById('id_designer').value = id;
document.getElementById('id_designer_search').value = name;
document.getElementById('designer-search-results').innerHTML = '';
}
</script>

View File

@@ -0,0 +1,26 @@
{% if error %}
<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
{{ error }}
</div>
{% else %}
<div class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert">
{% if manufacturer.id %}
Manufacturer "{{ manufacturer.name }}" has been created successfully.
<script>
// Update the manufacturer field in the parent form
selectManufacturer('{{ manufacturer.id }}', '{{ manufacturer.name }}');
// Close the modal
document.dispatchEvent(new CustomEvent('close-manufacturer-modal'));
</script>
{% else %}
Your manufacturer submission "{{ manufacturer.name }}" has been sent for review.
You will be notified when it is approved.
<script>
// Close the modal after a short delay
setTimeout(() => {
document.dispatchEvent(new CustomEvent('close-manufacturer-modal'));
}, 2000);
</script>
{% endif %}
</div>
{% endif %}

View File

@@ -0,0 +1,84 @@
{% 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);
htmx.ajax('POST', '/rides/manufacturers/create/', {
values: Object.fromEntries(formData),
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
}).then(response => {
if (response.detail) {
const data = JSON.parse(response.detail.xhr.response);
selectManufacturer(data.id, data.name);
}
$dispatch('close-manufacturer-modal');
}).finally(() => {
submitting = false;
});
}">
{% csrf_token %}
<div id="manufacturer-form-notification"></div>
<div class="space-y-6">
<!-- Name -->
<div>
<label for="manufacturer_name" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name *
</label>
<input type="text"
name="name"
id="manufacturer_name"
value="{{ search_term|default:'' }}"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
required>
</div>
{% if not user.is_privileged %}
<!-- Reason and Source for non-privileged users -->
<div>
<label for="reason" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Reason for Addition *
</label>
<textarea name="reason"
id="reason"
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="3"
required
placeholder="Please explain why you're adding this manufacturer and provide any relevant details."></textarea>
</div>
<div>
<label for="source" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Source (Optional)
</label>
<input type="text"
name="source"
id="source"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="URL or reference for this information">
</div>
{% endif %}
</div>
<div class="flex justify-end mt-6 space-x-3">
{% if modal %}
<button type="button"
@click="$dispatch('close-manufacturer-modal')"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600">
Cancel
</button>
{% endif %}
<button type="submit"
:disabled="submitting"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 dark:hover:bg-blue-500 disabled:opacity-50">
<span x-show="!submitting">Create Manufacturer</span>
<span x-show="submitting">Creating...</span>
</button>
</div>
</form>

View File

@@ -0,0 +1,33 @@
<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;">
{% if manufacturers %}
{% for manufacturer in manufacturers %}
<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="selectManufacturer('{{ manufacturer.id }}', '{{ manufacturer.name|escapejs }}')">
{{ manufacturer.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No matches found. You can still submit this name.
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectManufacturer(id, name) {
document.getElementById('id_manufacturer').value = id;
document.getElementById('id_manufacturer_search').value = name;
document.getElementById('manufacturer-search-results').innerHTML = '';
// Update ride model search to include manufacturer
const rideModelSearch = document.getElementById('id_ride_model_search');
if (rideModelSearch) {
rideModelSearch.setAttribute('hx-include', '[name="manufacturer"]');
}
}
</script>

View File

@@ -0,0 +1,291 @@
{% load static %}
<script>
function selectManufacturer(id, name) {
document.getElementById('id_manufacturer').value = id;
document.getElementById('id_manufacturer_search').value = name;
document.getElementById('manufacturer-search-results').innerHTML = '';
// Update ride model search to include manufacturer
const rideModelSearch = document.getElementById('id_ride_model_search');
if (rideModelSearch) {
rideModelSearch.setAttribute('hx-include', '[name="manufacturer"]');
}
}
function selectDesigner(id, name) {
document.getElementById('id_designer').value = id;
document.getElementById('id_designer_search').value = name;
document.getElementById('designer-search-results').innerHTML = '';
}
function selectRideModel(id, name) {
document.getElementById('id_ride_model').value = id;
document.getElementById('id_ride_model_search').value = name;
document.getElementById('ride-model-search-results').innerHTML = '';
}
// Handle form submission
document.addEventListener('submit', function(e) {
if (e.target.id === 'ride-form') {
// Clear search results
document.getElementById('manufacturer-search-results').innerHTML = '';
document.getElementById('designer-search-results').innerHTML = '';
document.getElementById('ride-model-search-results').innerHTML = '';
}
});
// Handle clicks outside search results
document.addEventListener('click', function(e) {
const manufacturerResults = document.getElementById('manufacturer-search-results');
const designerResults = document.getElementById('designer-search-results');
const rideModelResults = document.getElementById('ride-model-search-results');
if (!e.target.closest('#manufacturer-search-container')) {
manufacturerResults.innerHTML = '';
}
if (!e.target.closest('#designer-search-container')) {
designerResults.innerHTML = '';
}
if (!e.target.closest('#ride-model-search-container')) {
rideModelResults.innerHTML = '';
}
});
</script>
<form method="post" id="ride-form" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %}
<!-- Park Area -->
{% if form.park_area %}
<div class="space-y-2">
<label for="{{ form.park_area.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Park Area
</label>
{{ form.park_area }}
{% if form.park_area.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.park_area.errors }}
</div>
{% endif %}
</div>
{% endif %}
<!-- Name -->
<div class="space-y-2">
<label for="{{ form.name.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Name *
</label>
{{ form.name }}
{% if form.name.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.name.errors }}
</div>
{% endif %}
</div>
<!-- Manufacturer -->
<div class="space-y-2">
<div id="manufacturer-search-container" class="relative">
<label for="{{ form.manufacturer_search.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Manufacturer
</label>
{{ form.manufacturer_search }}
{{ form.manufacturer }}
<div id="manufacturer-search-results" class="relative"></div>
{% if form.manufacturer.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.manufacturer.errors }}
</div>
{% endif %}
</div>
</div>
<!-- Designer -->
<div class="space-y-2">
<div id="designer-search-container" class="relative">
<label for="{{ form.designer_search.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Designer
</label>
{{ form.designer_search }}
{{ form.designer }}
<div id="designer-search-results" class="relative"></div>
{% if form.designer.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.designer.errors }}
</div>
{% endif %}
</div>
</div>
<!-- Ride Model -->
<div class="space-y-2">
<div id="ride-model-search-container" class="relative">
<label for="{{ form.ride_model_search.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Ride Model
</label>
{{ form.ride_model_search }}
{{ form.ride_model }}
<div id="ride-model-search-results" class="relative"></div>
{% if form.ride_model.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.ride_model.errors }}
</div>
{% endif %}
</div>
</div>
<!-- Model Name -->
<div class="space-y-2">
<label for="{{ form.model_name.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Model Name
</label>
{{ form.model_name }}
{% if form.model_name.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.model_name.errors }}
</div>
{% endif %}
</div>
<!-- Category -->
<div class="space-y-2">
<label for="{{ form.category.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Category *
</label>
{{ form.category }}
{% if form.category.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.category.errors }}
</div>
{% endif %}
</div>
<!-- Coaster Fields -->
<div id="coaster-fields"></div>
<!-- Status -->
<div class="space-y-2">
<label for="{{ form.status.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Status
</label>
{{ form.status }}
{% if form.status.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.status.errors }}
</div>
{% endif %}
</div>
<!-- Opening Date -->
<div class="space-y-2">
<label for="{{ form.opening_date.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Opening Date
</label>
{{ form.opening_date }}
{% if form.opening_date.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.opening_date.errors }}
</div>
{% endif %}
</div>
<!-- Closing Date -->
<div class="space-y-2">
<label for="{{ form.closing_date.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Closing Date
</label>
{{ form.closing_date }}
{% if form.closing_date.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.closing_date.errors }}
</div>
{% endif %}
</div>
<!-- Status Since -->
<div class="space-y-2">
<label for="{{ form.status_since.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Status Since
</label>
{{ form.status_since }}
{% if form.status_since.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.status_since.errors }}
</div>
{% endif %}
</div>
<!-- Min Height -->
<div class="space-y-2">
<label for="{{ form.min_height_in.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Minimum Height (inches)
</label>
{{ form.min_height_in }}
{% if form.min_height_in.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.min_height_in.errors }}
</div>
{% endif %}
</div>
<!-- Max Height -->
<div class="space-y-2">
<label for="{{ form.max_height_in.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Maximum Height (inches)
</label>
{{ form.max_height_in }}
{% if form.max_height_in.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.max_height_in.errors }}
</div>
{% endif %}
</div>
<!-- Capacity -->
<div class="space-y-2">
<label for="{{ form.capacity_per_hour.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Hourly Capacity
</label>
{{ form.capacity_per_hour }}
{% if form.capacity_per_hour.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.capacity_per_hour.errors }}
</div>
{% endif %}
</div>
<!-- Ride Duration -->
<div class="space-y-2">
<label for="{{ form.ride_duration_seconds.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Ride Duration (seconds)
</label>
{{ form.ride_duration_seconds }}
{% if form.ride_duration_seconds.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.ride_duration_seconds.errors }}
</div>
{% endif %}
</div>
<!-- Description -->
<div class="space-y-2">
<label for="{{ form.description.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Description
</label>
{{ form.description }}
{% if form.description.errors %}
<div class="text-sm text-red-600 dark:text-red-400">
{{ form.description.errors }}
</div>
{% endif %}
</div>
<!-- Submit Button -->
<div class="flex justify-end">
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600">
{% if is_edit %}Save Changes{% else %}Add Ride{% endif %}
</button>
</div>
</form>

View File

@@ -0,0 +1,44 @@
{% if error %}
<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
<span class="font-medium">Error:</span> {{ error }}
</div>
{% elif ride_model.id %}
<div class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert">
<span class="font-medium">Success!</span> Ride model "{{ ride_model.name }}" has been created.
</div>
<script>
// Update the ride model field in the parent form
const rideModelId = '{{ ride_model.id }}';
const rideModelName = '{{ ride_model.name|escapejs }}';
document.getElementById('id_ride_model').value = rideModelId;
document.getElementById('id_ride_model_search').value = rideModelName;
// Close the modal after a short delay to allow the user to see the success message
setTimeout(() => {
document.dispatchEvent(new CustomEvent('close-ride-model-modal'));
// Clear the notification after the modal closes
setTimeout(() => {
document.getElementById('ride-model-notification').innerHTML = '';
}, 300);
}, 1000);
</script>
{% else %}
<div class="p-4 mb-4 text-sm text-yellow-700 bg-yellow-100 rounded-lg dark:bg-yellow-200 dark:text-yellow-800" role="alert">
<span class="font-medium">Note:</span> Your submission has been sent for review. You will be notified when it is approved.
</div>
<script>
// Close the modal after a short delay to allow the user to see the message
setTimeout(() => {
document.dispatchEvent(new CustomEvent('close-ride-model-modal'));
// Clear the notification after the modal closes
setTimeout(() => {
document.getElementById('ride-model-notification').innerHTML = '';
}, 300);
}, 2000);
</script>
{% endif %}

View File

@@ -0,0 +1,215 @@
{% 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
}
}).then(response => {
if (response.detail) {
const data = JSON.parse(response.detail.xhr.response);
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);
}
}
}).finally(() => {
submitting = false;
});
}">
{% csrf_token %}
<div id="ride-model-notification"></div>
<div class="space-y-6">
<!-- Name -->
<div>
<label for="{{ form.name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ form.name.label }}{% if form.name.field.required %} *{% endif %}
</label>
<input type="text"
name="name"
id="{{ form.name.id_for_label }}"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
required>
</div>
<!-- Manufacturer Search -->
<div>
<label for="{{ form.manufacturer_search.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ form.manufacturer_search.label }}
</label>
<div class="relative">
<div id="manufacturer-notification" class="mb-2"></div>
<div class="flex gap-2">
<div class="flex-grow">
<input type="text"
id="{{ form.manufacturer_search.id_for_label }}"
name="manufacturer_search"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Search for a manufacturer..."
hx-get="/rides/search/manufacturers/"
hx-trigger="click, input changed delay:200ms"
hx-target="#manufacturer-search-results"
autocomplete="off"
@input="manufacturerSearchTerm = $event.target.value"
{% if prefilled_manufacturer %}
value="{{ prefilled_manufacturer.name }}"
readonly
{% endif %}>
</div>
{% if not prefilled_manufacturer and not create_ride_model %}
<button type="button"
class="px-3 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
@click.prevent="setManufacturerModal(true, manufacturerSearchTerm)">
<i class="fas fa-plus"></i>
</button>
{% endif %}
</div>
<div id="manufacturer-search-results" class="absolute z-50 w-full"></div>
</div>
<input type="hidden"
name="manufacturer"
id="{{ form.manufacturer.id_for_label }}"
{% if prefilled_manufacturer %}
value="{{ prefilled_manufacturer.id }}"
{% endif %}>
</div>
<!-- Category -->
<div>
<label for="{{ form.category.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ form.category.label }}{% if form.category.field.required %} *{% endif %}
</label>
<select name="category"
id="{{ form.category.id_for_label }}"
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white"
required>
{% for value, label in form.category.field.choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<!-- Description -->
<div>
<label for="{{ form.description.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ form.description.label }}{% if form.description.field.required %} *{% endif %}
</label>
<textarea name="description"
id="{{ form.description.id_for_label }}"
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="4"></textarea>
</div>
{% if not user.is_privileged %}
<!-- Reason and Source for non-privileged users -->
<div>
<label for="reason" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Reason for Addition *
</label>
<textarea name="reason"
id="reason"
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="3"
required
placeholder="Please explain why you're adding this ride model and provide any relevant details."></textarea>
</div>
<div>
<label for="source" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Source (Optional)
</label>
<input type="text"
name="source"
id="source"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="URL or reference for this information">
</div>
{% endif %}
</div>
<div class="flex justify-end mt-6 space-x-3">
{% if modal %}
<button type="button"
@click="$dispatch('close-ride-model-modal')"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600">
Cancel
</button>
{% endif %}
<button type="submit"
:disabled="submitting"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 dark:hover:bg-blue-500 disabled:opacity-50">
<span x-show="!submitting">Create Ride Model</span>
<span x-show="submitting">Creating...</span>
</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

@@ -0,0 +1,38 @@
<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;">
{% if not manufacturer_id %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
Please select a manufacturer first
</div>
{% else %}
{% if ride_models %}
{% for ride_model in ride_models %}
<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="selectRideModel('{{ ride_model.id }}', '{{ ride_model.name|escapejs }}')">
{{ ride_model.name }}
{% if ride_model.manufacturer %}
<div class="text-sm text-gray-700 dark:text-gray-300">
by {{ ride_model.manufacturer.name }}
</div>
{% endif %}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No matches found. You can still submit this name.
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
{% endif %}
</div>
<script>
function selectRideModel(id, name) {
document.getElementById('id_ride_model').value = id;
document.getElementById('id_ride_model_search').value = name;
document.getElementById('ride-model-search-results').innerHTML = '';
}
</script>