mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 12:11:13 -05:00
- Add complete backend/ directory with full Django application - Add frontend/ directory with Vite + TypeScript setup ready for Next.js - Add comprehensive shared/ directory with: - Complete documentation and memory-bank archives - Media files and avatars (letters, park/ride images) - Deployment scripts and automation tools - Shared types and utilities - Add architecture/ directory with migration guides - Configure pnpm workspace for monorepo development - Update .gitignore to exclude .django_tailwind_cli/ build artifacts - Preserve all historical documentation in shared/docs/memory-bank/ - Set up proper structure for full-stack development with shared resources
280 lines
11 KiB
HTML
280 lines
11 KiB
HTML
{% 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>
|