mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 17:31:10 -05:00
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:
139
templates/rides/park_category_list.html
Normal file
139
templates/rides/park_category_list.html
Normal file
@@ -0,0 +1,139 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
{% load ride_tags %}
|
||||
|
||||
{% block title %}
|
||||
{% if park %}
|
||||
{{ category }} at {{ park.name }} - ThrillWiki
|
||||
{% else %}
|
||||
{{ category }} - ThrillWiki
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container px-4 py-8 mx-auto">
|
||||
<div class="mb-8">
|
||||
{% if park %}
|
||||
<h1 class="mb-2 text-3xl font-bold">{{ category }} at {{ park.name }}</h1>
|
||||
<a href="{% url 'parks:park_detail' park.slug %}" class="text-blue-600 hover:text-blue-800">← Back to {{ park.name }}</a>
|
||||
{% else %}
|
||||
<h1 class="mb-2 text-3xl font-bold">{{ category }}</h1>
|
||||
<a href="{% url 'rides:ride_list' %}" class="text-blue-600 hover:text-blue-800">← Back to All Rides</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if rides %}
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{% for ride in rides %}
|
||||
<div class="overflow-hidden transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
|
||||
<div class="aspect-w-16 aspect-h-9">
|
||||
{% if ride.photos.exists %}
|
||||
<img src="{{ ride.photos.first.image.url }}"
|
||||
alt="{{ ride.name }}"
|
||||
class="object-cover w-full">
|
||||
{% else %}
|
||||
<img src="{% get_ride_placeholder_image ride.category %}"
|
||||
alt="{{ ride.name }}"
|
||||
class="object-cover w-full">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<h2 class="mb-2 text-xl font-bold">
|
||||
{% if park %}
|
||||
<a href="{% url 'parks:rides:ride_detail' park.slug ride.slug %}"
|
||||
class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
|
||||
{{ ride.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'rides:ride_detail' ride.slug %}"
|
||||
class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
|
||||
{{ ride.name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
{% if not park %}
|
||||
<p class="mb-3 text-gray-600 dark:text-gray-400">
|
||||
at <a href="{% url 'parks:park_detail' ride.park.slug %}"
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
{{ ride.park.name }}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.ride_model %}
|
||||
<p class="mb-2 text-gray-600 dark:text-gray-400">
|
||||
Model: {{ ride.ride_model.name }}
|
||||
{% if ride.ride_model.manufacturer %}
|
||||
by {{ ride.ride_model.manufacturer.name }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="status-badge {% if ride.status == 'OPERATING' %}status-operating
|
||||
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
|
||||
{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction
|
||||
{% elif ride.status == 'DEMOLISHED' %}status-demolished
|
||||
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
|
||||
{{ ride.get_status_display }}
|
||||
</span>
|
||||
{% if ride.average_rating %}
|
||||
<span class="text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-400/30 dark:text-yellow-200 dark:ring-1 dark:ring-yellow-400/30">
|
||||
<i class="mr-1 text-yellow-500 fas fa-star dark:text-yellow-300"></i>
|
||||
{{ ride.average_rating|floatformat:1 }}/10
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if ride.coaster_stats %}
|
||||
<div class="grid grid-cols-2 gap-2 mt-4">
|
||||
{% if ride.coaster_stats.height_ft %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Height: {{ ride.coaster_stats.height_ft }}ft
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if ride.coaster_stats.speed_mph %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Speed: {{ ride.coaster_stats.speed_mph }}mph
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-8 text-center rounded-lg bg-gray-50 dark:bg-gray-800">
|
||||
{% if park %}
|
||||
<p class="text-gray-600 dark:text-gray-400">No {{ category|lower }} found at this park.</p>
|
||||
{% else %}
|
||||
<p class="text-gray-600 dark:text-gray-400">No {{ category|lower }} found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="flex justify-center mt-6">
|
||||
<div class="inline-flex rounded-md shadow-sm">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">« First</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-2 text-gray-700 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Next</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Last »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
47
templates/rides/partials/add_ride_modal.html
Normal file
47
templates/rides/partials/add_ride_modal.html
Normal 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>
|
||||
110
templates/rides/partials/coaster_fields.html
Normal file
110
templates/rides/partials/coaster_fields.html
Normal 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>
|
||||
93
templates/rides/partials/create_ride_model_form.html
Normal file
93
templates/rides/partials/create_ride_model_form.html
Normal 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>
|
||||
26
templates/rides/partials/designer_created.html
Normal file
26
templates/rides/partials/designer_created.html
Normal 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 %}
|
||||
84
templates/rides/partials/designer_form.html
Normal file
84
templates/rides/partials/designer_form.html
Normal 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>
|
||||
27
templates/rides/partials/designer_search_results.html
Normal file
27
templates/rides/partials/designer_search_results.html
Normal 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>
|
||||
26
templates/rides/partials/manufacturer_created.html
Normal file
26
templates/rides/partials/manufacturer_created.html
Normal 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 %}
|
||||
84
templates/rides/partials/manufacturer_form.html
Normal file
84
templates/rides/partials/manufacturer_form.html
Normal 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>
|
||||
33
templates/rides/partials/manufacturer_search_results.html
Normal file
33
templates/rides/partials/manufacturer_search_results.html
Normal 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>
|
||||
291
templates/rides/partials/ride_form.html
Normal file
291
templates/rides/partials/ride_form.html
Normal 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>
|
||||
44
templates/rides/partials/ride_model_created.html
Normal file
44
templates/rides/partials/ride_model_created.html
Normal 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 %}
|
||||
215
templates/rides/partials/ride_model_form.html
Normal file
215
templates/rides/partials/ride_model_form.html
Normal 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>
|
||||
38
templates/rides/partials/ride_model_search_results.html
Normal file
38
templates/rides/partials/ride_model_search_results.html
Normal 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>
|
||||
@@ -8,7 +8,7 @@
|
||||
<!-- Action Buttons - Above header -->
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex justify-end gap-2 mb-2">
|
||||
<a href="{% url 'parks:rides:ride_edit' park_slug=ride.park.slug ride_slug=ride.slug %}"
|
||||
<a href="{% url 'parks:rides:ride_update' park_slug=ride.park.slug ride_slug=ride.slug %}"
|
||||
class="transition-transform btn-secondary hover:scale-105">
|
||||
<i class="mr-1 fas fa-pencil-alt"></i>Edit
|
||||
</a>
|
||||
@@ -84,11 +84,7 @@
|
||||
<dt class="text-sm font-semibold text-gray-900 sm:text-base lg:text-lg dark:text-white">Length</dt>
|
||||
<dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl lg:text-3xl dark:text-sky-400">{{ coaster_stats.length_ft }} ft</dd>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
|
||||
<dt class="text-sm font-semibold text-gray-900 sm:text-base lg:text-lg dark:text-white">Length</dt>
|
||||
<dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl lg:text-3xl dark:text-sky-400">N/A</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -101,6 +97,10 @@
|
||||
<dd>
|
||||
<a href="{% url 'companies:manufacturer_detail' ride.manufacturer.slug %}"
|
||||
class="text-xs text-blue-600 sm:text-sm lg:text-base hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
{{ ride.manufacturer.name }}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.designer %}
|
||||
@@ -436,24 +436,22 @@
|
||||
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">History</h2>
|
||||
<div class="space-y-4">
|
||||
{% for record in history %}
|
||||
{% for record in ride.get_history %}
|
||||
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-700/50">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ record.history_date|date:"M d, Y H:i" }}
|
||||
{% if record.history_user %}
|
||||
by {{ record.history_user.username }}
|
||||
{% if record.history_user_display %}
|
||||
by {{ record.history_user_display }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{% for field, changes in record.diff_against_previous.items %}
|
||||
{% if field != "updated_at" %}
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">{{ field|title }}:</span>
|
||||
<span class="text-red-600 dark:text-red-400">{{ changes.old }}</span>
|
||||
<span class="mx-1">→</span>
|
||||
<span class="text-green-600 dark:text-green-400">{{ changes.new }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">{{ field|title }}:</span>
|
||||
<span class="text-red-600 dark:text-red-400">{{ changes.old }}</span>
|
||||
<span class="mx-1">→</span>
|
||||
<span class="text-green-600 dark:text-green-400">{{ changes.new }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -485,7 +483,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'js/photo-gallery.js' %}"></script>
|
||||
|
||||
@@ -1,114 +1,245 @@
|
||||
{% extends 'base/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if is_edit %}Edit {{ ride.name }} {% else %}Add Ride {% endif %}at {{ park.name }} - ThrillWiki{% endblock %}
|
||||
{% block title %}{% if is_edit %}Edit{% else %}Add{% endif %} Ride - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<!-- Ride Form -->
|
||||
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{% if is_edit %}Edit {{ ride.name }}{% else %}Add Ride{% endif %} at {{ park.name }}</h1>
|
||||
{% if is_edit %}
|
||||
<a href="{% url 'parks:rides:ride_detail' ride.park.slug ride.slug %}" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
Back to {{ ride.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'parks:park_detail' park.slug %}" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
Back to {{ park.name }} Rides
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="container px-4 py-8 mx-auto">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h1 class="mb-6 text-3xl font-bold">
|
||||
{% if is_edit %}Edit {{ object.name }}{% else %}Add New Ride{% endif %}
|
||||
{% if park %}
|
||||
<div class="mt-2 text-lg font-normal text-gray-600 dark:text-gray-400">
|
||||
at <a href="{% url 'parks:park_detail' park.slug %}" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">{{ park.name }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="space-y-6" x-data="{
|
||||
status: '{{ form.instance.status|default:'OPERATING' }}',
|
||||
clearResults(containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (container && !container.contains(event.target)) {
|
||||
container.querySelector('[id$=search-results]').innerHTML = '';
|
||||
}
|
||||
},
|
||||
handleStatusChange(event) {
|
||||
this.status = event.target.value;
|
||||
if (this.status === 'CLOSING') {
|
||||
document.getElementById('id_closing_date').required = true;
|
||||
} else {
|
||||
document.getElementById('id_closing_date').required = false;
|
||||
}
|
||||
},
|
||||
showClosingDate() {
|
||||
return ['CLOSING', 'SBNO', 'CLOSED_PERM', 'DEMOLISHED', 'RELOCATED'].includes(this.status);
|
||||
}
|
||||
}">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if not park %}
|
||||
{# Park Selection - Only shown when creating from global view #}
|
||||
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold">Park Information</h2>
|
||||
<div class="space-y-4">
|
||||
<div id="park-search-container" class="relative" @click.outside="clearResults('park-search-container')">
|
||||
<label for="{{ form.park_search.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Park *
|
||||
</label>
|
||||
{{ form.park_search }}
|
||||
{{ form.park }}
|
||||
<div id="park-search-results" class="relative"></div>
|
||||
{% if form.park.errors %}
|
||||
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{{ form.park.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Basic Information #}
|
||||
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold">Basic Information</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="col-span-2">
|
||||
<label for="{{ form.name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Name *
|
||||
</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{{ form.name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<label for="{{ form.category.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Category *
|
||||
</label>
|
||||
{{ form.category }}
|
||||
{% if form.category.errors %}
|
||||
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{{ form.category.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="coaster-fields" class="col-span-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" class="space-y-6">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
{% if field.name != 'park' %}
|
||||
<div>
|
||||
<label for="{{ field.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
{% if field.field.widget.input_type == 'text' or field.field.widget.input_type == 'date' or field.field.widget.input_type == 'number' %}
|
||||
<input type="{{ field.field.widget.input_type }}"
|
||||
name="{{ field.name }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
value="{{ field.value|default:'' }}"
|
||||
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
{% if field.field.required %}required{% endif %}>
|
||||
{% elif field.field.widget.input_type == 'select' %}
|
||||
<select name="{{ field.name }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
{% if field.field.required %}required{% endif %}>
|
||||
{% for value, label in field.field.choices %}
|
||||
<option value="{{ value }}" {% if field.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elif field.field.widget.input_type == 'textarea' %}
|
||||
<textarea name="{{ field.name }}"
|
||||
id="{{ field.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"
|
||||
{% if field.field.required %}required{% endif %}>{{ field.value|default:'' }}</textarea>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{# Manufacturer and Model #}
|
||||
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold">Manufacturer and Model</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div id="manufacturer-search-container" class="relative" @click.outside="clearResults('manufacturer-search-container')">
|
||||
<label for="{{ form.manufacturer_search.id_for_label }}" class="block mb-1 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>
|
||||
</div>
|
||||
|
||||
{% if not user.role == 'MODERATOR' and not user.role == 'ADMIN' and not user.role == 'SUPERUSER' %}
|
||||
<div id="designer-search-container" class="relative" @click.outside="clearResults('designer-search-container')">
|
||||
<label for="{{ form.designer_search.id_for_label }}" class="block mb-1 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>
|
||||
</div>
|
||||
|
||||
<div id="ride-model-search-container" class="relative col-span-2" @click.outside="clearResults('ride-model-search-container')">
|
||||
<label for="{{ form.ride_model_search.id_for_label }}" class="block mb-1 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Status and Dates #}
|
||||
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold">Status and Dates</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="{{ form.status.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Status
|
||||
</label>
|
||||
{{ form.status }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="{{ form.status_since.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Status Since
|
||||
</label>
|
||||
{{ form.status_since }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="{{ form.opening_date.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Opening Date
|
||||
</label>
|
||||
{{ form.opening_date }}
|
||||
</div>
|
||||
|
||||
<div x-show="showClosingDate()">
|
||||
<label for="{{ form.closing_date.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Closing Date <span x-show="status === 'CLOSING'" class="text-red-600">*</span>
|
||||
</label>
|
||||
{{ form.closing_date }}
|
||||
</div>
|
||||
|
||||
<div x-show="status === 'CLOSING'" class="col-span-2">
|
||||
<label for="{{ form.post_closing_status.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Status After Closing *
|
||||
</label>
|
||||
{{ form.post_closing_status }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Specifications #}
|
||||
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold">Specifications</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="{{ form.min_height_in.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Minimum Height (inches)
|
||||
</label>
|
||||
{{ form.min_height_in }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="{{ form.max_height_in.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Maximum Height (inches)
|
||||
</label>
|
||||
{{ form.max_height_in }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="{{ form.capacity_per_hour.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Hourly Capacity
|
||||
</label>
|
||||
{{ form.capacity_per_hour }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="{{ form.ride_duration_seconds.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Ride Duration (seconds)
|
||||
</label>
|
||||
{{ form.ride_duration_seconds }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Description #}
|
||||
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold">Description</h2>
|
||||
<div>
|
||||
<label for="{{ form.description.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Description
|
||||
</label>
|
||||
{{ form.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Submission Details #}
|
||||
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold">Submission Details</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="reason" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Reason for {% if is_edit %}Edit{% else %}Addition{% endif %}
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Reason for Changes
|
||||
</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 {% if is_edit %}editing{% else %}adding{% endif %} this ride and provide any relevant details."></textarea>
|
||||
<textarea name="reason" rows="2"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Explain why you're making these changes"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="source" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Source (Optional)
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Source
|
||||
</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="Link to official website, news article, or other source">
|
||||
<input type="text" name="source"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Where did you get this information?">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<a href="{% if is_edit %}{% url 'parks:rides:ride_detail' park.slug object.slug %}{% else %}{% url 'parks:rides:ride_list' park.slug %}{% endif %}"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">
|
||||
{% if is_edit %}Save Changes{% else %}Submit{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Photos Section (only shown on edit) -->
|
||||
{% if is_edit %}
|
||||
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800" id="photos">
|
||||
{% include "media/partials/photo_manager.html" with photos=object.photos.all content_type="rides.ride" object_id=object.id %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Submit Button #}
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="px-6 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800">
|
||||
{% if is_edit %}Save Changes{% else %}Add Ride{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -23,10 +23,22 @@
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">All Rides</h1>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if park and user.is_authenticated %}
|
||||
<a href="{% url 'parks:rides:ride_create' park.slug %}" class="btn-primary">
|
||||
<i class="mr-2 fas fa-plus"></i>Add Ride
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
{% if park %}
|
||||
<a href="{% url 'parks:rides:ride_create' park.slug %}" 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
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'rides:ride_create' %}" 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
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user