okay fine

This commit is contained in:
pacnpal
2024-11-03 17:47:26 +00:00
parent 01c6004a79
commit 27eb239e97
10020 changed files with 1935769 additions and 2364 deletions

View File

@@ -90,7 +90,7 @@
<i class="fas fa-map-marker-alt"></i>
<span>Parks</span>
</a>
<a href="{% url 'parks:all_rides' %}" class="nav-link">
<a href="{% url 'rides:ride_list' %}" class="nav-link">
<i class="fas fa-rocket"></i>
<span>Rides</span>
</a>

View File

@@ -0,0 +1,17 @@
<div class="search-results-container">
{% if results %}
{% for result in results %}
<div class="p-2 cursor-pointer hover:bg-gray-100"
data-action="click->location-map#selectLocation"
data-result="{{ result|json }}">
<div class="font-medium">{{ result.name }}</div>
<div class="text-sm text-gray-500">
{% if result.address.city %}{{ result.address.city }}, {% endif %}
{{ result.address.country }}
</div>
</div>
{% endfor %}
{% else %}
<div class="p-2 text-gray-500">No results found</div>
{% endif %}
</div>

View File

@@ -0,0 +1,279 @@
{% load static %}
<style>
/* Ensure map container and its elements stay below other UI elements */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
z-index: 1 !important;
}
.leaflet-control {
z-index: 2 !important;
}
</style>
<div class="location-widget" id="locationWidget">
{# Search Form #}
<div class="relative mb-4">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Search Location
</label>
<input type="text"
id="locationSearch"
class="relative 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="Search for a location..."
autocomplete="off"
style="z-index: 10;">
<div id="searchResults"
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
class="hidden w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600">
</div>
</div>
{# Map Container #}
<div class="relative mb-4" style="z-index: 1;">
<div id="locationMap" class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
</div>
{# Location Form Fields #}
<div class="relative grid grid-cols-1 gap-4 md:grid-cols-2" style="z-index: 10;">
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name
</label>
<input type="text"
name="location_name"
id="locationName"
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"
value="{{ form.location_name.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Type
</label>
<input type="text"
name="location_type"
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"
value="amusement_park"
readonly>
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Street Address
</label>
<input type="text"
name="street_address"
id="streetAddress"
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"
value="{{ form.street_address.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
City
</label>
<input type="text"
name="city"
id="city"
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"
value="{{ form.city.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
State/Region
</label>
<input type="text"
name="state"
id="state"
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"
value="{{ form.state.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Country
</label>
<input type="text"
name="country"
id="country"
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"
value="{{ form.country.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Postal Code
</label>
<input type="text"
name="postal_code"
id="postalCode"
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"
value="{{ form.postal_code.value|default:'' }}">
</div>
</div>
{# Hidden Coordinate Fields #}
<div class="hidden">
<input type="hidden" name="latitude" id="latitude" value="{{ form.latitude.value|default:'' }}">
<input type="hidden" name="longitude" id="longitude" value="{{ form.longitude.value|default:'' }}">
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let map = null;
let marker = null;
const searchInput = document.getElementById('locationSearch');
const searchResults = document.getElementById('searchResults');
let searchTimeout;
// Initialize map
function initMap() {
map = L.map('locationMap').setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Initialize with existing coordinates if available
const initialLat = document.getElementById('latitude').value;
const initialLng = document.getElementById('longitude').value;
if (initialLat && initialLng) {
addMarker(parseFloat(initialLat), parseFloat(initialLng));
}
// Handle map clicks
map.on('click', async function(e) {
const { lat, lng } = e.latlng;
try {
const response = await fetch(`/parks/search/reverse-geocode/?lat=${lat}&lon=${lng}`);
const data = await response.json();
updateLocation(lat, lng, data);
} catch (error) {
console.error('Reverse geocoding failed:', error);
}
});
}
// Initialize map
initMap();
// Handle location search
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (!query) {
searchResults.classList.add('hidden');
return;
}
searchTimeout = setTimeout(async function() {
try {
const response = await fetch(`/parks/search/location/?q=${encodeURIComponent(query)}`);
const data = await response.json();
if (data.results && data.results.length > 0) {
const resultsHtml = data.results.map((result, index) => `
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
data-result-index="${index}">
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
${(result.address && result.address.city) ? result.address.city + ', ' : ''}${(result.address && result.address.country) || ''}
</div>
</div>
`).join('');
searchResults.innerHTML = resultsHtml;
searchResults.classList.remove('hidden');
// Store results data
searchResults.dataset.results = JSON.stringify(data.results);
// Add click handlers
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
el.addEventListener('click', function() {
const results = JSON.parse(searchResults.dataset.results);
const result = results[this.dataset.resultIndex];
selectLocation(result);
});
});
} else {
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
searchResults.classList.remove('hidden');
}
} catch (error) {
console.error('Search failed:', error);
}
}, 300);
});
// Hide search results when clicking outside
document.addEventListener('click', function(e) {
if (!searchResults.contains(e.target) && e.target !== searchInput) {
searchResults.classList.add('hidden');
}
});
function addMarker(lat, lng) {
if (marker) {
marker.remove();
}
marker = L.marker([lat, lng]).addTo(map);
map.setView([lat, lng], 13);
}
function updateLocation(lat, lng, data) {
// Update coordinates
document.getElementById('latitude').value = lat || '';
document.getElementById('longitude').value = lng || '';
// Update marker
if (lat && lng) {
addMarker(lat, lng);
}
// Update form fields
const address = data.address || {};
document.getElementById('locationName').value = data.name || data.display_name || '';
document.getElementById('streetAddress').value =
`${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
document.getElementById('city').value =
address.city || address.town || address.village || '';
document.getElementById('state').value =
address.state || address.region || '';
document.getElementById('country').value = address.country || '';
document.getElementById('postalCode').value = address.postcode || '';
}
function selectLocation(result) {
if (!result) return;
const lat = parseFloat(result.lat);
const lon = parseFloat(result.lon);
if (isNaN(lat) || isNaN(lon)) return;
// Create a normalized address object
const address = {
name: result.display_name || result.name || '',
address: {
house_number: result.address ? result.address.house_number : '',
road: result.address ? (result.address.road || result.address.street) : '',
city: result.address ? (result.address.city || result.address.town || result.address.village) : '',
state: result.address ? (result.address.state || result.address.region) : '',
country: result.address ? result.address.country : '',
postcode: result.address ? result.address.postcode : ''
}
};
updateLocation(lat, lon, address);
searchResults.classList.add('hidden');
searchInput.value = address.name;
}
});
</script>

View File

@@ -1,29 +1,44 @@
{% extends 'base/base.html' %}
{% extends "base/base.html" %}
{% load static %}
{% block title %}{{ park.name }} - ThrillWiki{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<!-- Park Header -->
<div class="p-6 mb-6 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700">
<div class="flex flex-col items-start justify-between md:flex-row md:items-center">
<!-- Header -->
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex items-start justify-between">
<div>
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1>
<p class="text-gray-600 dark:text-gray-300">
<i class="mr-2 fas fa-map-marker-alt"></i>
<span>{{ park.get_formatted_location }}</span>
</p>
</div>
<div class="flex gap-2 mt-4 md:mt-0">
{% if park.website %}
<a href="{{ park.website }}" target="_blank" rel="noopener noreferrer"
class="btn-secondary">
<i class="mr-2 fas fa-external-link-alt"></i>Visit Website
</a>
{% if park.city or park.state or park.country %}
<p class="mb-2 text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i>
{% if park.city %}{{ park.city }}{% endif %}
{% if park.city and park.state %}, {% endif %}
{% if park.state %}{{ park.state }}{% endif %}
{% if park.country and park.state or park.city %}, {% endif %}
{% if park.country %}{{ park.country }}{% endif %}
</p>
{% endif %}
{% if user.is_authenticated %}
<a href="{% url 'parks:park_edit' slug=park.slug %}" class="btn-secondary">
<div class="flex flex-wrap gap-2 mt-3">
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% if park.average_rating %}
<span class="flex items-center text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50">
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span>
{{ park.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</div>
</div>
{% if user.is_authenticated %}
<div class="flex gap-2">
<a href="{% url 'parks:park_update' park.slug %}" class="btn-secondary">
<i class="mr-2 fas fa-edit"></i>Edit
</a>
{% if perms.media.add_photo %}
@@ -31,223 +46,143 @@
<i class="mr-2 fas fa-camera"></i>Upload Photo
</button>
{% endif %}
{% endif %}
</div>
</div>
<div class="flex flex-wrap gap-2 mt-4">
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% if park.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>
{{ park.average_rating|floatformat:1 }}/10
</span>
</div>
{% endif %}
</div>
</div>
<!-- Photos -->
{% if park.photos.exists %}
<div class="p-6 mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-2xl font-bold text-gray-900 dark:text-white">Photos</h2>
<div class="p-6 mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2>
{% include "media/partials/photo_display.html" with photos=park.photos.all content_type="parks.park" object_id=park.id %}
</div>
{% endif %}
<!-- Park Stats -->
<div class="grid grid-cols-1 gap-6 mb-6 md:grid-cols-3">
<a href="{% url 'parks:rides:ride_list' park.slug %}" class="p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{{ rides.count }}
</div>
<div class="mt-1 text-gray-600 dark:text-gray-300">Total Attractions</div>
</a>
<div class="p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{% with roller_coasters=rides|dictsortreversed:"category"|slice:":RC" %}
{{ roller_coasters|length }}
{% endwith %}
</div>
<div class="mt-1 text-gray-600 dark:text-gray-300">Roller Coasters</div>
</div>
<div class="p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{{ areas.count }}
</div>
<div class="mt-1 text-gray-600 dark:text-gray-300">Areas</div>
</div>
</div>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Column - Description and Areas -->
<!-- Left Column - Description and Details -->
<div class="lg:col-span-2">
{% if park.description %}
<div class="p-6 mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-2xl font-bold text-gray-900 dark:text-white">About</h2>
<div class="text-gray-700 dark:text-gray-300">
<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">About</h2>
<div class="prose dark:prose-invert max-w-none">
{{ park.description|linebreaks }}
</div>
</div>
{% endif %}
<!-- Park Areas -->
{% if areas %}
<div class="p-6 mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-2xl font-bold text-gray-900 dark:text-white">Areas</h2>
<div class="grid gap-4">
{% for area in areas %}
<div class="p-4 transition-transform transform rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:-translate-y-1">
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
{{ area.name }}
</h3>
{% if area.description %}
<p class="mb-2 text-gray-600 dark:text-gray-300">
{{ area.description }}
</p>
{% endif %}
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ area.rides.count }} attractions
</div>
</div>
{% endfor %}
</div>
<!-- Location Map -->
{% if park.latitude and park.longitude %}
<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">Location</h2>
<div id="map" class="relative h-[400px] rounded-lg" style="z-index: 0;"></div>
</div>
{% endif %}
<!-- Rides List -->
<div class="p-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<!-- Rides and Attractions -->
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Attractions</h2>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Rides & Attractions</h2>
<a href="{% url 'parks:rides:ride_list' park.slug %}" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
View All <i class="ml-1 fas fa-arrow-right"></i>
View All
</a>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% for ride in rides %}
<div class="p-4 transition-transform transform rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:-translate-y-1">
<h3 class="mb-2 text-lg font-semibold">
<a href="{% url 'parks:rides:ride_detail' park.slug ride.slug %}"
class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
{{ ride.name }}
{% if park.rides.exists %}
<div class="grid gap-4 md:grid-cols-2">
{% for ride in park.rides.all|slice:":6" %}
<div class="p-4 transition-colors rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700">
<a href="{% url 'parks:rides:ride_detail' park.slug ride.slug %}" class="block">
<h3 class="mb-1 font-semibold text-gray-900 dark:text-white">{{ ride.name }}</h3>
<span class="text-blue-800 bg-blue-100 status-badge dark:bg-blue-700 dark:text-blue-50">
{{ ride.get_category_display }}
</span>
{% if ride.average_rating %}
<span class="flex items-center text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50">
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span>
{{ ride.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</a>
</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-blue-800 bg-blue-100 status-badge dark:bg-blue-400/30 dark:text-blue-200 dark:ring-1 dark:ring-blue-400/30">
{{ ride.get_category_display }}
</span>
<span class="status-badge {% if ride.status == 'OPERATING' %}status-operating
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
{% else %}status-construction{% endif %}">
{{ ride.get_status_display }}
</span>
</div>
{% if ride.coaster_stats %}
<div class="space-y-1 text-sm text-gray-600 dark:text-gray-400">
{% if ride.coaster_stats.height_ft %}
<div>Height: {{ ride.coaster_stats.height_ft }}ft</div>
{% endif %}
{% if ride.coaster_stats.speed_mph %}
<div>Speed: {{ ride.coaster_stats.speed_mph }}mph</div>
{% endif %}
</div>
{% endif %}
</div>
{% empty %}
<div class="col-span-2 py-8 text-center">
<p class="text-gray-500 dark:text-gray-400">No attractions found.</p>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-500 dark:text-gray-400">No rides or attractions listed yet.</p>
{% endif %}
</div>
</div>
<!-- Right Column - Quick Facts and History -->
<div>
<div class="p-6 mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-2xl font-bold text-gray-900 dark:text-white">Quick Facts</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-1">
<!-- Right Column - Quick Facts -->
<div class="lg:col-span-1">
<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">Quick Facts</h2>
<dl class="space-y-4">
{% if park.owner %}
<div class="p-4 transition-transform transform rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:-translate-y-1">
<dt class="flex items-center mb-1 text-gray-600 dark:text-gray-300">
<i class="w-5 text-blue-500 fas fa-building dark:text-blue-400"></i>
<span class="ml-2">Operator</span>
</dt>
<div>
<dt class="text-gray-500">Owner/Operator</dt>
<dd class="font-medium text-gray-900 dark:text-white">
<a href="{% url 'companies:company_detail' park.owner.slug %}"
class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ park.owner.name }}
</a>
</dd>
</div>
{% endif %}
<div class="p-4 transition-transform transform rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:-translate-y-1">
<dt class="flex items-center mb-1 text-gray-600 dark:text-gray-300">
<i class="w-5 text-blue-500 fas fa-globe dark:text-blue-400"></i>
<span class="ml-2">Location</span>
</dt>
<dd class="font-medium text-gray-900 dark:text-white">
{{ park.get_formatted_location }}
</dd>
</div>
{% if park.opening_date %}
<div class="p-4 transition-transform transform rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:-translate-y-1">
<dt class="flex items-center mb-1 text-gray-600 dark:text-gray-300">
<i class="w-5 text-blue-500 fas fa-calendar-alt dark:text-blue-400"></i>
<span class="ml-2">Opening Date</span>
</dt>
<dd class="font-medium text-gray-900 dark:text-white">
{{ park.opening_date }}
</dd>
<div>
<dt class="text-gray-500">Opening Date</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ park.opening_date }}</dd>
</div>
{% endif %}
{% if park.closing_date %}
<div class="p-4 transition-transform transform rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:-translate-y-1">
<dt class="flex items-center mb-1 text-gray-600 dark:text-gray-300">
<i class="w-5 text-blue-500 fas fa-calendar-times dark:text-blue-400"></i>
<span class="ml-2">Closing Date</span>
</dt>
<dd class="font-medium text-gray-900 dark:text-white">
{{ park.closing_date }}
</dd>
<div>
<dt class="text-gray-500">Closing Date</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ park.closing_date }}</dd>
</div>
{% endif %}
{% if park.operating_season %}
<div class="p-4 transition-transform transform rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:-translate-y-1">
<dt class="flex items-center mb-1 text-gray-600 dark:text-gray-300">
<i class="w-5 text-blue-500 fas fa-clock dark:text-blue-400"></i>
<span class="ml-2">Operating Season</span>
</dt>
<dd class="font-medium text-gray-900 dark:text-white">
{{ park.operating_season }}
</dd>
<div>
<dt class="text-gray-500">Operating Season</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ park.operating_season }}</dd>
</div>
{% endif %}
{% if park.size_acres %}
<div class="p-4 transition-transform transform rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:-translate-y-1">
<dt class="flex items-center mb-1 text-gray-600 dark:text-gray-300">
<i class="w-5 text-blue-500 fas fa-ruler-combined dark:text-blue-400"></i>
<span class="ml-2">Size</span>
</dt>
<div>
<dt class="text-gray-500">Size</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ park.size_acres }} acres</dd>
</div>
{% endif %}
{% if park.total_rides %}
<div>
<dt class="text-gray-500">Total Rides</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ park.total_rides }}</dd>
</div>
{% endif %}
{% if park.total_roller_coasters %}
<div>
<dt class="text-gray-500">Roller Coasters</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ park.total_roller_coasters }}</dd>
</div>
{% endif %}
{% if park.website %}
<div>
<dt class="text-gray-500">Website</dt>
<dd class="font-medium text-gray-900 dark:text-white">
{{ park.size_acres }} acres
<a href="{{ park.website }}"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
target="_blank" rel="noopener noreferrer">
{{ park.website }}
</a>
</dd>
</div>
{% endif %}
</div>
</dl>
</div>
<!-- History Panel -->
<div class="p-6 mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<h2 class="mb-4 text-2xl font-bold text-gray-900 dark:text-white">History</h2>
<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 %}
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-700/50">
@@ -280,7 +215,7 @@
<div x-data="{ show: false }"
@show-photo-upload.window="show = true"
x-show="show"
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
class="fixed inset-0 z-[60] flex items-center justify-center bg-black bg-opacity-50"
@click.self="show = false">
<div class="w-full max-w-2xl p-6 mx-4 bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
@@ -293,4 +228,29 @@
</div>
</div>
{% endif %}
{% if park.latitude and park.longitude %}
{% block extra_js %}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const map = L.map('map').setView([{{ park.latitude }}, {{ park.longitude }}], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
L.marker([{{ park.latitude }}, {{ park.longitude }}])
.addTo(map)
.bindPopup("{{ park.name }}");
});
</script>
{% endblock %}
{% endif %}
{% block extra_head %}
{% if park.latitude and park.longitude %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
{% endif %}
{% endblock %}
{% endblock %}

View File

@@ -1,241 +1,321 @@
{% extends 'base/base.html' %} {% load static %} {% block title %}{% if is_edit %}Edit {{ object.name }}{% else %}Add Park{% endif %} - ThrillWiki{% endblock %}
{% extends "base/base.html" %}
{% load static %}
{% block extra_head %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
.photo-preview {
transition: all 0.3s ease;
}
.photo-preview.uploading {
opacity: 0.5;
}
.photo-preview.error {
border-color: red;
}
.photo-preview.success {
border-color: green;
}
</style>
{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<div class="max-w-3xl mx-auto">
<!-- Park Form -->
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h1 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">
{% if is_edit %}Edit {{ object.name }}{% else %}Add Park{% endif %}
</h1>
<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{% else %}Create{% endif %} Park
</h1>
{% if form.errors %}
<div class="p-4 mb-6 text-red-700 bg-red-100 border border-red-400 rounded-lg dark:bg-red-900 dark:text-red-100 dark:border-red-700">
<p class="font-medium">Please correct the following errors:</p>
<ul class="ml-4 list-disc">
{% for field in form %} {% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %} {% endfor %} {% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post" enctype="multipart/form-data" class="space-y-6" x-data="parkForm()">
{% csrf_token %}
<form method="post" class="space-y-6" id="park-form">
{% csrf_token %}
{# 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 class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name
</label>
{{ form.name }}
</div>
<div class="col-span-2">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Description
</label>
{{ form.description }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Owner/Operator
</label>
{{ form.owner }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Status
</label>
{{ form.status }}
</div>
</div>
</div>
<!-- Hidden fields -->
{{ form.country }} {{ form.region }} {{ form.city }}
{# Location #}
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold">Location</h2>
{% include "parks/partials/location_widget.html" %}
</div>
<!-- Name field -->
<div>
<label for="{{ form.name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name *
</label>
<div>{{ form.name }}</div>
{% if form.name.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.name.errors }}
</div>
{% endif %}
</div>
{# Photos #}
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold">Photos</h2>
{# Existing Photos #}
{% if park.photos.exists %}
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 lg:grid-cols-3">
{% for photo in park.photos.all %}
<div class="relative overflow-hidden rounded-lg aspect-w-16 aspect-h-9">
<img src="{{ photo.image.url }}"
alt="{{ photo.caption|default:park.name }}"
class="object-cover w-full h-full">
<div class="absolute top-0 right-0 p-2">
<button type="button"
class="p-2 text-white bg-red-600 rounded-full hover:bg-red-700"
@click="removePhoto('{{ photo.id }}')">
<i class="fas fa-times"></i>
</button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Location fields -->
<div x-data="locationAutocomplete('country', false)" class="relative">
<label
for="{{ form.country_name.id_for_label }}"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
Country *
</label>
<input
type="text"
id="id_country_name"
name="country_name"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select country..."
value="{{ form.country_name.value|default:'' }}"
autocomplete="off"
/>
<!-- Suggestions Dropdown -->
<ul
x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60"
>
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li
@click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600"
></li>
</template>
</ul>
</div>
{# Photo Upload #}
<div class="space-y-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Add Photos
</label>
<div class="relative">
<input type="file"
multiple
accept="image/*"
class="hidden"
x-ref="fileInput"
@change="handleFileSelect">
<button type="button"
class="w-full px-4 py-2 text-gray-700 border-2 border-dashed rounded-lg dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
@click="$refs.fileInput.click()">
<span x-show="!previews.length">
<i class="mr-2 fas fa-upload"></i>
Click to upload photos
</span>
<span x-show="previews.length">
<i class="mr-2 fas fa-plus"></i>
Add more photos
</span>
</button>
</div>
<div x-data="locationAutocomplete('region', false)" class="relative">
<label
for="{{ form.region_name.id_for_label }}"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
Region/State
</label>
<input
type="text"
id="id_region_name"
name="region_name"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select region/state..."
value="{{ form.region_name.value|default:'' }}"
autocomplete="off"
/>
<!-- Suggestions Dropdown -->
<ul
x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60"
>
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li
@click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600"
></li>
</template>
</ul>
</div>
{# Photo Previews #}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<template x-for="(preview, index) in previews" :key="preview.id">
<div class="relative overflow-hidden transition-all duration-300 rounded-lg aspect-w-16 aspect-h-9 photo-preview"
:class="{
'uploading': preview.uploading,
'error': preview.error,
'success': preview.uploaded
}">
<img :src="preview.url"
class="object-cover w-full h-full">
<div class="absolute top-0 right-0 p-2">
<button type="button"
class="p-2 text-white bg-red-600 rounded-full hover:bg-red-700"
@click="removePreview(index)">
<i class="fas fa-times"></i>
</button>
</div>
<div x-show="preview.uploading"
class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div class="w-16 h-16 border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
</div>
<div x-show="preview.error"
class="absolute bottom-0 left-0 right-0 p-2 text-sm text-white bg-red-500">
Upload failed
</div>
</div>
</template>
</div>
</div>
</div>
<div x-data="locationAutocomplete('city', false)" class="relative">
<label
for="{{ form.city_name.id_for_label }}"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
City
</label>
<input
type="text"
id="id_city_name"
name="city_name"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select city..."
value="{{ form.city_name.value|default:'' }}"
autocomplete="off"
/>
<!-- Suggestions Dropdown -->
<ul
x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60"
>
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li
@click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600"
></li>
</template>
</ul>
</div>
{# Additional Details #}
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold">Additional Details</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Opening Date
</label>
{{ form.opening_date }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Closing Date
</label>
{{ form.closing_date }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Operating Season
</label>
{{ form.operating_season }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Size (acres)
</label>
{{ form.size_acres }}
</div>
<div class="col-span-2">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Website
</label>
{{ form.website }}
</div>
</div>
</div>
<!-- Other fields -->
{% for field in form %} {% if field.name not in 'name,country,region,city,country_name,region_name,city_name' %}
<div>
<label
for="{{ field.id_for_label }}"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ field.label }}{% if field.field.required %} *{% endif %}
</label>
<div>{{ field }}</div>
{% 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 %} {% if not user.role == 'MODERATOR' and not user.role == 'ADMIN' and not user.role == 'SUPERUSER' %}
<div
class="p-4 mb-4 text-blue-700 bg-blue-100 border border-blue-400 rounded-lg dark:bg-blue-900 dark:text-blue-100 dark:border-blue-700"
>
<p>
Your submission will be reviewed by a moderator before being
published.
</p>
</div>
<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>
<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 park 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="Link to official website, news article, or other source"
/>
</div>
</div>
{% endif %}
{# 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 class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Reason for Changes
</label>
<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 class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Source
</label>
<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>
</div>
<div class="flex justify-end space-x-4">
<a
href="{% if is_edit %}{% url 'parks:park_detail' slug=object.slug %}{% else %}{% url 'parks:park_list' %}{% 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>
{# 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"
:disabled="uploading"
x-text="uploading ? 'Uploading...' : '{% if is_edit %}Save Changes{% else %}Create Park{% 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="parks.park" object_id=object.id %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
function parkForm() {
return {
previews: [],
uploading: false,
handleFileSelect(event) {
const files = event.target.files;
if (!files.length) return;
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file.type.startsWith('image/')) continue;
const reader = new FileReader();
reader.onload = (e) => {
this.previews.push({
id: Date.now() + i,
file: file,
url: e.target.result,
uploading: false,
uploaded: false,
error: false
});
};
reader.readAsDataURL(file);
}
// Reset file input
event.target.value = '';
},
removePreview(index) {
this.previews.splice(index, 1);
},
async uploadPhotos() {
if (!this.previews.length) return true;
this.uploading = true;
let allUploaded = true;
for (let preview of this.previews) {
if (preview.uploaded || preview.error) continue;
preview.uploading = true;
const formData = new FormData();
formData.append('image', preview.file);
formData.append('app_label', 'parks');
formData.append('model', 'park');
formData.append('object_id', '{{ park.id }}');
try {
const response = await fetch('/photos/upload/', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
},
body: formData
});
if (!response.ok) throw new Error('Upload failed');
const result = await response.json();
preview.uploading = false;
preview.uploaded = true;
} catch (error) {
console.error('Upload failed:', error);
preview.uploading = false;
preview.error = true;
allUploaded = false;
}
}
this.uploading = false;
return allUploaded;
},
removePhoto(photoId) {
if (confirm('Are you sure you want to remove this photo?')) {
fetch(`/photos/${photoId}/delete/`, {
method: 'DELETE',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
},
}).then(response => {
if (response.ok) {
window.location.reload();
}
});
}
}
}
}
</script>
{% endblock %}

View File

@@ -30,26 +30,26 @@
</div>
<!-- Country Field -->
<div x-data="locationAutocomplete('country', true)" class="relative">
<div x-data="locationSearch('country')" class="relative">
<label for="country" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Country</label>
<input type="text"
name="country"
id="country"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
@input.debounce.300ms="search()"
@focus="search()"
@click.away="results = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select country..."
value="{{ current_filters.country }}"
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
<!-- Results Dropdown -->
<ul x-show="results.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion.name"
<template x-for="result in results" :key="result.address.country">
<li @click="select(result)"
x-text="result.address.country"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
@@ -57,26 +57,26 @@
</div>
<!-- Region Field -->
<div x-data="locationAutocomplete('region', true)" class="relative">
<div x-data="locationSearch('state')" class="relative">
<label for="region" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">State/Region</label>
<input type="text"
name="region"
id="region"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
@input.debounce.300ms="search()"
@focus="search()"
@click.away="results = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select state/region..."
value="{{ current_filters.region }}"
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
<!-- Results Dropdown -->
<ul x-show="results.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion.name"
<template x-for="result in results" :key="result.address.state">
<li @click="select(result)"
x-text="result.address.state"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
@@ -84,26 +84,26 @@
</div>
<!-- City Field -->
<div x-data="locationAutocomplete('city', true)" class="relative">
<div x-data="locationSearch('city')" class="relative">
<label for="city" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">City</label>
<input type="text"
name="city"
id="city"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
@input.debounce.300ms="search()"
@focus="search()"
@click.away="results = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select city..."
value="{{ current_filters.city }}"
autocomplete="off">
<!-- Suggestions Dropdown -->
<ul x-show="suggestions.length > 0"
<!-- Results Dropdown -->
<ul x-show="results.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li @click="selectSuggestion(suggestion)"
x-text="suggestion.name"
<template x-for="result in results" :key="result.address.city">
<li @click="select(result)"
x-text="result.address.city"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
</li>
</template>
@@ -165,6 +165,45 @@
</div>
<script>
function locationSearch(type) {
return {
query: '',
results: [],
async search() {
if (!this.query.trim()) {
this.results = [];
return;
}
try {
const response = await fetch(`/location/search/?q=${encodeURIComponent(this.query)}&type=${type}&filter_parks=true`);
const data = await response.json();
this.results = data.results;
} catch (error) {
console.error('Search failed:', error);
this.results = [];
}
},
select(result) {
let value = '';
switch (type) {
case 'country':
value = result.address.country;
break;
case 'state':
value = result.address.state;
break;
case 'city':
value = result.address.city;
break;
}
this.query = value;
this.results = [];
document.getElementById('park-filters').dispatchEvent(new Event('change'));
}
};
}
function toggleStatus(button, status) {
const form = document.getElementById('park-filters');
const existingInputs = form.querySelectorAll(`input[name="status"][value="${status}"]`);

View File

@@ -1,4 +0,0 @@
<option value="">---------</option>
{% for city in cities %}
<option value="{{ city.pk }}">{{ city.name }}</option>
{% endfor %}

View File

@@ -1,15 +0,0 @@
<div class="absolute z-10 w-full bg-white border rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600">
{% for country in countries %}
<div class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
hx-post="{% url 'parks:select_country' %}"
hx-target="#country-input"
hx-swap="value"
hx-vals='{"country": "{{ country.name }}"}'>
{{ country.name }}
</div>
{% empty %}
<div class="px-4 py-2 text-gray-500 dark:text-gray-400">
No countries found
</div>
{% endfor %}
</div>

View File

@@ -0,0 +1,350 @@
{% load static %}
<style>
/* Ensure map container and its elements stay below other UI elements */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
z-index: 1 !important;
}
.leaflet-control {
z-index: 2 !important;
}
</style>
<div class="location-widget" id="locationWidget">
{# Search Form #}
<div class="relative mb-4">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Search Location
</label>
<input type="text"
id="locationSearch"
class="relative 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="Search for a location..."
autocomplete="off"
style="z-index: 10;">
<div id="searchResults"
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
class="hidden w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600">
</div>
</div>
{# Map Container #}
<div class="relative mb-4" style="z-index: -1;">
<div id="locationMap" class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
</div>
{# Location Form Fields #}
<div class="relative grid grid-cols-1 gap-4 md:grid-cols-2" style="z-index: 10;">
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Street Address
</label>
<input type="text"
name="street_address"
id="streetAddress"
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"
value="{{ form.street_address.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
City
</label>
<input type="text"
name="city"
id="city"
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"
value="{{ form.city.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
State/Region
</label>
<input type="text"
name="state"
id="state"
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"
value="{{ form.state.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Country
</label>
<input type="text"
name="country"
id="country"
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"
value="{{ form.country.value|default:'' }}">
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Postal Code
</label>
<input type="text"
name="postal_code"
id="postalCode"
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"
value="{{ form.postal_code.value|default:'' }}">
</div>
</div>
{# Hidden Coordinate Fields #}
<div class="hidden">
<input type="hidden" name="latitude" id="latitude" value="{{ form.latitude.value|default:'' }}">
<input type="hidden" name="longitude" id="longitude" value="{{ form.longitude.value|default:'' }}">
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let map = null;
let marker = null;
const searchInput = document.getElementById('locationSearch');
const searchResults = document.getElementById('searchResults');
let searchTimeout;
function normalizeCoordinate(value, maxDigits, decimalPlaces) {
try {
// Convert to string with exact decimal places
const rounded = Number(value).toFixed(decimalPlaces);
// Convert to string without decimal point for digit counting
const strValue = rounded.replace('.', '').replace('-', '');
// Remove trailing zeros
const strippedValue = strValue.replace(/0+$/, '');
// If total digits exceed maxDigits, round further
if (strippedValue.length > maxDigits) {
return Number(Number(value).toFixed(decimalPlaces - 1));
}
// Return the string representation to preserve exact decimal places
return rounded;
} catch (error) {
console.error('Coordinate normalization failed:', error);
return null;
}
}
function validateCoordinates(lat, lng) {
// Normalize coordinates
const normalizedLat = normalizeCoordinate(lat, 9, 6);
const normalizedLng = normalizeCoordinate(lng, 10, 6);
if (normalizedLat === null || normalizedLng === null) {
throw new Error('Invalid coordinate format');
}
const parsedLat = parseFloat(normalizedLat);
const parsedLng = parseFloat(normalizedLng);
if (parsedLat < -90 || parsedLat > 90) {
throw new Error('Latitude must be between -90 and 90 degrees.');
}
if (parsedLng < -180 || parsedLng > 180) {
throw new Error('Longitude must be between -180 and 180 degrees.');
}
return { lat: normalizedLat, lng: normalizedLng };
}
// Initialize map
function initMap() {
map = L.map('locationMap').setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Initialize with existing coordinates if available
const initialLat = document.getElementById('latitude').value;
const initialLng = document.getElementById('longitude').value;
if (initialLat && initialLng) {
try {
const normalized = validateCoordinates(initialLat, initialLng);
addMarker(normalized.lat, normalized.lng);
} catch (error) {
console.error('Invalid initial coordinates:', error);
}
}
// Handle map clicks
map.on('click', async function(e) {
try {
const normalized = validateCoordinates(e.latlng.lat, e.latlng.lng);
const response = await fetch(`/parks/search/reverse-geocode/?lat=${normalized.lat}&lon=${normalized.lng}`);
if (!response.ok) {
throw new Error('Geocoding request failed');
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
updateLocation(normalized.lat, normalized.lng, data);
} catch (error) {
console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.');
}
});
}
// Initialize map
initMap();
// Handle location search
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (!query) {
searchResults.classList.add('hidden');
return;
}
searchTimeout = setTimeout(async function() {
try {
const response = await fetch(`/parks/search/location/?q=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error('Search request failed');
}
const data = await response.json();
if (data.results && data.results.length > 0) {
const resultsHtml = data.results.map((result, index) => `
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
data-result-index="${index}">
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
${(result.address && result.address.city) ? result.address.city + ', ' : ''}${(result.address && result.address.country) || ''}
</div>
</div>
`).join('');
searchResults.innerHTML = resultsHtml;
searchResults.classList.remove('hidden');
// Store results data
searchResults.dataset.results = JSON.stringify(data.results);
// Add click handlers
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
el.addEventListener('click', function() {
const results = JSON.parse(searchResults.dataset.results);
const result = results[this.dataset.resultIndex];
selectLocation(result);
});
});
} else {
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
searchResults.classList.remove('hidden');
}
} catch (error) {
console.error('Search failed:', error);
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
searchResults.classList.remove('hidden');
}
}, 300);
});
// Hide search results when clicking outside
document.addEventListener('click', function(e) {
if (!searchResults.contains(e.target) && e.target !== searchInput) {
searchResults.classList.add('hidden');
}
});
function addMarker(lat, lng) {
if (marker) {
marker.remove();
}
marker = L.marker([lat, lng]).addTo(map);
map.setView([lat, lng], 13);
}
function updateLocation(lat, lng, data) {
try {
const normalized = validateCoordinates(lat, lng);
// Update coordinates
document.getElementById('latitude').value = normalized.lat;
document.getElementById('longitude').value = normalized.lng;
// Update marker
addMarker(normalized.lat, normalized.lng);
// Update form fields with English names where available
const address = data.address || {};
document.getElementById('streetAddress').value =
`${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
document.getElementById('city').value =
address.city || address.town || address.village || '';
document.getElementById('state').value =
address.state || address.region || '';
document.getElementById('country').value = address.country || '';
document.getElementById('postalCode').value = address.postcode || '';
} catch (error) {
console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.');
}
}
function selectLocation(result) {
if (!result) return;
try {
const lat = parseFloat(result.lat);
const lon = parseFloat(result.lon);
if (isNaN(lat) || isNaN(lon)) {
throw new Error('Invalid coordinates in search result');
}
const normalized = validateCoordinates(lat, lon);
// Create a normalized address object
const address = {
name: result.display_name || result.name || '',
address: {
house_number: result.address ? result.address.house_number : '',
road: result.address ? (result.address.road || result.address.street) : '',
city: result.address ? (result.address.city || result.address.town || result.address.village) : '',
state: result.address ? (result.address.state || result.address.region) : '',
country: result.address ? result.address.country : '',
postcode: result.address ? result.address.postcode : ''
}
};
updateLocation(normalized.lat, normalized.lng, address);
searchResults.classList.add('hidden');
searchInput.value = address.name;
} catch (error) {
console.error('Location selection failed:', error);
alert(error.message || 'Failed to select location. Please try again.');
}
}
// Add form submit handler
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
const lat = document.getElementById('latitude').value;
const lng = document.getElementById('longitude').value;
if (lat && lng) {
try {
validateCoordinates(lat, lng);
} catch (error) {
e.preventDefault();
alert(error.message || 'Invalid coordinates. Please check the location.');
}
}
});
});
</script>

View File

@@ -16,10 +16,16 @@
{{ park.name }}
</a>
</h2>
<p class="mb-3 text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i>
{{ park.get_formatted_location }}
</p>
{% if park.city or park.state or park.country %}
<p class="mb-3 text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i>
{% if park.city %}{{ park.city }}{% endif %}
{% if park.city and park.state %}, {% endif %}
{% if park.state %}{{ park.state }}{% endif %}
{% if park.country and park.state or park.city %}, {% endif %}
{% if park.country %}{{ park.country }}{% endif %}
</p>
{% endif %}
<div class="flex flex-wrap gap-2">
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
@@ -35,18 +41,13 @@
</span>
{% endif %}
</div>
<div class="flex items-center justify-between mt-4">
<a href="{% url 'parks:rides:ride_list' park.slug %}"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ park.rides.count }} attractions <i class="ml-1 fas fa-arrow-right"></i>
</a>
{% if park.owner %}
<a href="{% url 'companies:company_detail' park.owner.slug %}"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{% if park.owner %}
<div class="mt-4 text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
<a href="{% url 'companies:company_detail' park.owner.slug %}">
{{ park.owner.name }}
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
{% empty %}

View File

@@ -1,4 +0,0 @@
<option value="">---------</option>
{% for region in regions %}
<option value="{{ region.pk }}">{{ region.name }}</option>
{% endfor %}

View File

@@ -33,7 +33,7 @@
<!-- Filters -->
<div class="p-4 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<form class="grid grid-cols-1 gap-4 md:grid-cols-3"
hx-get="{% if park %}{% url 'parks:rides:ride_list' park.slug %}{% else %}{% url 'parks:all_rides' %}{% endif %}"
hx-get="{% if park %}{% url 'parks:rides:ride_list' park.slug %}{% else %}{% url 'rides:ride_list' %}{% endif %}"
hx-trigger="change from:select, input from:input[type='text']"
hx-target="#rides-grid"
hx-push-url="true">