mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:51:09 -05:00
Refactor park and ride detail templates to utilize Alpine.js for state management in photo galleries and upload modals. Enhanced photo handling and initialization logic for improved user experience.
This commit is contained in:
@@ -12,14 +12,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script nonce="{{ request.csp_nonce }}">
|
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
Alpine.data('photoUploadModal', () => ({
|
|
||||||
show: false,
|
|
||||||
editingPhoto: { caption: '' }
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container px-4 mx-auto sm:px-6 lg:px-8">
|
<div class="container px-4 mx-auto sm:px-6 lg:px-8">
|
||||||
<!-- Action Buttons - Above header -->
|
<!-- Action Buttons - Above header -->
|
||||||
@@ -141,7 +133,16 @@
|
|||||||
|
|
||||||
<!-- Rest of the content remains unchanged -->
|
<!-- Rest of the content remains unchanged -->
|
||||||
{% if park.photos.exists %}
|
{% if park.photos.exists %}
|
||||||
<div class="p-optimized mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
<div class="p-optimized mb-8 bg-white rounded-lg shadow dark:bg-gray-800"
|
||||||
|
x-data="{
|
||||||
|
selectedPhoto: null,
|
||||||
|
showGallery: false,
|
||||||
|
currentIndex: 0,
|
||||||
|
photos: {{ park.photos.all|length }},
|
||||||
|
init() {
|
||||||
|
// Photo gallery initialization
|
||||||
|
}
|
||||||
|
}">
|
||||||
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2>
|
<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 %}
|
{% include "media/partials/photo_display.html" with photos=park.photos.all content_type="parks.park" object_id=park.id %}
|
||||||
</div>
|
</div>
|
||||||
@@ -188,10 +189,12 @@
|
|||||||
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Location</h2>
|
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Location</h2>
|
||||||
{% with location=park.location.first %}
|
{% with location=park.location.first %}
|
||||||
{% if location.latitude is not None and location.longitude is not None %}
|
{% if location.latitude is not None and location.longitude is not None %}
|
||||||
<div id="park-map" class="relative rounded-lg" style="z-index: 0;"
|
<div x-data="parkMap"
|
||||||
data-latitude="{{ location.latitude|default_if_none:'' }}"
|
x-init="initMap({{ location.latitude }}, {{ location.longitude }}, '{{ park.name|escapejs }}')"
|
||||||
data-longitude="{{ location.longitude|default_if_none:'' }}"
|
id="park-map"
|
||||||
data-park-name="{{ park.name|escape }}"></div>
|
class="relative rounded-lg h-64"
|
||||||
|
style="z-index: 0;">
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="relative rounded-lg p-4 text-center text-gray-500 dark:text-gray-400">
|
<div class="relative rounded-lg p-4 text-center text-gray-500 dark:text-gray-400">
|
||||||
<i class="fas fa-map-marker-alt text-2xl mb-2"></i>
|
<i class="fas fa-map-marker-alt text-2xl mb-2"></i>
|
||||||
@@ -239,13 +242,7 @@
|
|||||||
<!-- Photo Upload Modal -->
|
<!-- Photo Upload Modal -->
|
||||||
{% if perms.media.add_photo %}
|
{% if perms.media.add_photo %}
|
||||||
<div x-cloak
|
<div x-cloak
|
||||||
x-data="{
|
x-data="photoUploadModal"
|
||||||
show: false,
|
|
||||||
editingPhoto: null,
|
|
||||||
init() {
|
|
||||||
this.editingPhoto = { caption: '' };
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
@show-photo-upload.window="show = true; init()"
|
@show-photo-upload.window="show = true; init()"
|
||||||
x-show="show"
|
x-show="show"
|
||||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50"
|
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50"
|
||||||
@@ -266,27 +263,39 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<!-- Photo Gallery Script -->
|
<!-- External libraries only (Leaflet for maps) -->
|
||||||
<script src="{% static 'js/photo-gallery.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- Map Script (if location exists) -->
|
|
||||||
{% if park.location.exists %}
|
{% if park.location.exists %}
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<script src="{% static 'js/park-map.js' %}"></script>
|
{% endif %}
|
||||||
|
|
||||||
<script nonce="{{ request.csp_nonce }}">
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('alpine:init', () => {
|
||||||
var mapElement = document.getElementById('park-map');
|
// Photo Upload Modal Component
|
||||||
if (mapElement && mapElement.dataset.latitude && mapElement.dataset.longitude) {
|
Alpine.data('photoUploadModal', () => ({
|
||||||
var latitude = parseFloat(mapElement.dataset.latitude);
|
show: false,
|
||||||
var longitude = parseFloat(mapElement.dataset.longitude);
|
editingPhoto: null,
|
||||||
var parkName = mapElement.dataset.parkName;
|
init() {
|
||||||
|
this.editingPhoto = { caption: '' };
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
if (!isNaN(latitude) && !isNaN(longitude) && parkName) {
|
// Park Map Component
|
||||||
initParkMap(latitude, longitude, parkName);
|
{% if park.location.exists %}
|
||||||
|
Alpine.data('parkMap', () => ({
|
||||||
|
map: null,
|
||||||
|
initMap(lat, lng, parkName) {
|
||||||
|
if (typeof L !== 'undefined') {
|
||||||
|
this.map = L.map(this.$el).setView([lat, lng], 15);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors'
|
||||||
|
}).addTo(this.map);
|
||||||
|
L.marker([lat, lng]).addTo(this.map)
|
||||||
|
.bindPopup(parkName)
|
||||||
|
.openPopup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
{% if is_edit %}Edit{% else %}Create{% endif %} Park
|
{% if is_edit %}Edit{% else %}Create{% endif %} Park
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data" class="space-y-6" x-data="parkForm">
|
<form method="post" enctype="multipart/form-data" class="space-y-6" x-data="parkFormData">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{# Basic Information #}
|
{# Basic Information #}
|
||||||
@@ -81,7 +81,10 @@
|
|||||||
<div class="absolute top-0 right-0 p-2">
|
<div class="absolute top-0 right-0 p-2">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="p-2 text-white bg-red-600 rounded-full hover:bg-red-700"
|
class="p-2 text-white bg-red-600 rounded-full hover:bg-red-700"
|
||||||
@click="removePhoto('{{ photo.id }}')">
|
hx-delete="{% url 'media:photo_delete' photo.id %}"
|
||||||
|
hx-confirm="Are you sure you want to remove this photo?"
|
||||||
|
hx-target="closest .relative"
|
||||||
|
hx-swap="outerHTML">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +104,7 @@
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
x-ref="fileInput"
|
x-ref="fileInput"
|
||||||
@change="handleFileSelect">
|
@change="handleFileSelect($event)">
|
||||||
<button type="button"
|
<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"
|
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()">
|
@click="$refs.fileInput.click()">
|
||||||
@@ -212,8 +215,9 @@
|
|||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="px-6 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800"
|
class="px-6 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800"
|
||||||
:disabled="uploading"
|
:disabled="uploading">
|
||||||
x-text="uploading ? 'Uploading...' : '{% if is_edit %}Save Changes{% else %}Create Park{% endif %}'">
|
<span x-show="!uploading">{% if is_edit %}Save Changes{% else %}Create Park{% endif %}</span>
|
||||||
|
<span x-show="uploading">Uploading...</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -224,8 +228,8 @@
|
|||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function parkForm() {
|
document.addEventListener('alpine:init', () => {
|
||||||
return {
|
Alpine.data('parkFormData', () => ({
|
||||||
previews: [],
|
previews: [],
|
||||||
uploading: false,
|
uploading: false,
|
||||||
|
|
||||||
@@ -257,133 +261,8 @@ function parkForm() {
|
|||||||
|
|
||||||
removePreview(index) {
|
removePreview(index) {
|
||||||
this.previews.splice(index, 1);
|
this.previews.splice(index, 1);
|
||||||
},
|
|
||||||
|
|
||||||
uploadPhotos() {
|
|
||||||
if (!this.previews.length) return true;
|
|
||||||
|
|
||||||
this.uploading = true;
|
|
||||||
let allUploaded = true;
|
|
||||||
let uploadPromises = [];
|
|
||||||
|
|
||||||
for (let preview of this.previews) {
|
|
||||||
if (preview.uploaded || preview.error) continue;
|
|
||||||
|
|
||||||
preview.uploading = true;
|
|
||||||
|
|
||||||
// Create temporary form for HTMX request
|
|
||||||
const tempForm = document.createElement('form');
|
|
||||||
tempForm.setAttribute('hx-post', '/photos/upload/');
|
|
||||||
tempForm.setAttribute('hx-trigger', 'submit');
|
|
||||||
tempForm.setAttribute('hx-swap', 'none');
|
|
||||||
tempForm.enctype = 'multipart/form-data';
|
|
||||||
|
|
||||||
// Add CSRF token
|
|
||||||
const csrfInput = document.createElement('input');
|
|
||||||
csrfInput.type = 'hidden';
|
|
||||||
csrfInput.name = 'csrfmiddlewaretoken';
|
|
||||||
csrfInput.value = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
|
||||||
tempForm.appendChild(csrfInput);
|
|
||||||
|
|
||||||
// Add form data
|
|
||||||
const imageInput = document.createElement('input');
|
|
||||||
imageInput.type = 'file';
|
|
||||||
imageInput.name = 'image';
|
|
||||||
imageInput.files = this.createFileList([preview.file]);
|
|
||||||
tempForm.appendChild(imageInput);
|
|
||||||
|
|
||||||
const appLabelInput = document.createElement('input');
|
|
||||||
appLabelInput.type = 'hidden';
|
|
||||||
appLabelInput.name = 'app_label';
|
|
||||||
appLabelInput.value = 'parks';
|
|
||||||
tempForm.appendChild(appLabelInput);
|
|
||||||
|
|
||||||
const modelInput = document.createElement('input');
|
|
||||||
modelInput.type = 'hidden';
|
|
||||||
modelInput.name = 'model';
|
|
||||||
modelInput.value = 'park';
|
|
||||||
tempForm.appendChild(modelInput);
|
|
||||||
|
|
||||||
const objectIdInput = document.createElement('input');
|
|
||||||
objectIdInput.type = 'hidden';
|
|
||||||
objectIdInput.name = 'object_id';
|
|
||||||
objectIdInput.value = '{{ park.id }}';
|
|
||||||
tempForm.appendChild(objectIdInput);
|
|
||||||
|
|
||||||
// Track upload completion with event listeners
|
|
||||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
|
||||||
try {
|
|
||||||
if (event.detail.xhr.status === 200) {
|
|
||||||
const result = JSON.parse(event.detail.xhr.responseText);
|
|
||||||
preview.uploading = false;
|
|
||||||
preview.uploaded = true;
|
|
||||||
} else {
|
|
||||||
throw new Error('Upload failed');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}));
|
||||||
console.error('Upload failed:', error);
|
|
||||||
preview.uploading = false;
|
|
||||||
preview.error = true;
|
|
||||||
allUploaded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track completion
|
|
||||||
completedUploads++;
|
|
||||||
if (completedUploads === totalUploads) {
|
|
||||||
this.uploading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(tempForm);
|
|
||||||
});
|
});
|
||||||
document.body.appendChild(tempForm);
|
|
||||||
htmx.trigger(tempForm, 'submit');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize completion tracking
|
|
||||||
let completedUploads = 0;
|
|
||||||
const totalUploads = this.previews.filter(p => !p.uploaded && !p.error).length;
|
|
||||||
|
|
||||||
if (totalUploads === 0) {
|
|
||||||
this.uploading = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true; // Return immediately, completion handled by event listeners
|
|
||||||
},
|
|
||||||
|
|
||||||
createFileList(files) {
|
|
||||||
const dt = new DataTransfer();
|
|
||||||
files.forEach(file => dt.items.add(file));
|
|
||||||
return dt.files;
|
|
||||||
},
|
|
||||||
|
|
||||||
removePhoto(photoId) {
|
|
||||||
if (confirm('Are you sure you want to remove this photo?')) {
|
|
||||||
// Create temporary form for HTMX request
|
|
||||||
const tempForm = document.createElement('form');
|
|
||||||
tempForm.setAttribute('hx-delete', `/photos/${photoId}/delete/`);
|
|
||||||
tempForm.setAttribute('hx-trigger', 'submit');
|
|
||||||
tempForm.setAttribute('hx-swap', 'none');
|
|
||||||
|
|
||||||
// Add CSRF token
|
|
||||||
const csrfInput = document.createElement('input');
|
|
||||||
csrfInput.type = 'hidden';
|
|
||||||
csrfInput.name = 'csrfmiddlewaretoken';
|
|
||||||
csrfInput.value = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
|
||||||
tempForm.appendChild(csrfInput);
|
|
||||||
|
|
||||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
|
||||||
if (event.detail.xhr.status === 200) {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
document.body.removeChild(tempForm);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(tempForm);
|
|
||||||
htmx.trigger(tempForm, 'submit');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -149,7 +149,16 @@
|
|||||||
|
|
||||||
<!-- Rest of the content remains unchanged -->
|
<!-- Rest of the content remains unchanged -->
|
||||||
{% if ride.photos.exists %}
|
{% if ride.photos.exists %}
|
||||||
<div class="p-6 mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
<div class="p-6 mb-8 bg-white rounded-lg shadow dark:bg-gray-800"
|
||||||
|
x-data="{
|
||||||
|
selectedPhoto: null,
|
||||||
|
showGallery: false,
|
||||||
|
currentIndex: 0,
|
||||||
|
photos: {{ ride.photos.all|length }},
|
||||||
|
init() {
|
||||||
|
// Photo gallery initialization
|
||||||
|
}
|
||||||
|
}">
|
||||||
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2>
|
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2>
|
||||||
{% include "media/partials/photo_display.html" with photos=ride.photos.all content_type="rides.ride" object_id=ride.id %}
|
{% include "media/partials/photo_display.html" with photos=ride.photos.all content_type="rides.ride" object_id=ride.id %}
|
||||||
</div>
|
</div>
|
||||||
@@ -435,7 +444,7 @@
|
|||||||
<!-- Photo Upload Modal -->
|
<!-- Photo Upload Modal -->
|
||||||
{% if perms.media.add_photo %}
|
{% if perms.media.add_photo %}
|
||||||
<div x-cloak
|
<div x-cloak
|
||||||
x-data="{ show: false }"
|
x-data="photoUploadModal"
|
||||||
@show-photo-upload.window="show = true"
|
@show-photo-upload.window="show = true"
|
||||||
x-show="show"
|
x-show="show"
|
||||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50"
|
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50"
|
||||||
@@ -454,5 +463,15 @@
|
|||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="{% static 'js/photo-gallery.js' %}"></script>
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
// Photo Upload Modal Component
|
||||||
|
Alpine.data('photoUploadModal', () => ({
|
||||||
|
show: false,
|
||||||
|
init() {
|
||||||
|
// Modal initialization
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user