mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:31:07 -05:00
Remove unused JavaScript files: alerts.js, django-htmx.js, park-map.js, search.js, main.js, and photo-gallery.js
This commit is contained in:
@@ -1,18 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Get all alert elements
|
|
||||||
const alerts = document.querySelectorAll('.alert');
|
|
||||||
|
|
||||||
// For each alert
|
|
||||||
alerts.forEach(alert => {
|
|
||||||
// After 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
// Add slideOut animation
|
|
||||||
alert.style.animation = 'slideOut 0.5s ease-out forwards';
|
|
||||||
|
|
||||||
// Remove the alert after animation completes
|
|
||||||
setTimeout(() => {
|
|
||||||
alert.remove();
|
|
||||||
}, 500);
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
5
static/js/alpine.min.js
vendored
5
static/js/alpine.min.js
vendored
File diff suppressed because one or more lines are too long
5
static/js/cdn.min.js
vendored
5
static/js/cdn.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,81 +0,0 @@
|
|||||||
function locationAutocomplete(field, filterParks = false) {
|
|
||||||
return {
|
|
||||||
query: '',
|
|
||||||
suggestions: [],
|
|
||||||
fetchSuggestions() {
|
|
||||||
let url;
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
q: this.query,
|
|
||||||
filter_parks: filterParks
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (field) {
|
|
||||||
case 'country':
|
|
||||||
url = '/parks/ajax/countries/';
|
|
||||||
break;
|
|
||||||
case 'region':
|
|
||||||
url = '/parks/ajax/regions/';
|
|
||||||
// Add country parameter if we're fetching regions
|
|
||||||
const countryInput = document.getElementById(filterParks ? 'country' : 'id_country_name');
|
|
||||||
if (countryInput && countryInput.value) {
|
|
||||||
params.append('country', countryInput.value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'city':
|
|
||||||
url = '/parks/ajax/cities/';
|
|
||||||
// Add country and region parameters if we're fetching cities
|
|
||||||
const regionInput = document.getElementById(filterParks ? 'region' : 'id_region_name');
|
|
||||||
const cityCountryInput = document.getElementById(filterParks ? 'country' : 'id_country_name');
|
|
||||||
if (regionInput && regionInput.value && cityCountryInput && cityCountryInput.value) {
|
|
||||||
params.append('country', cityCountryInput.value);
|
|
||||||
params.append('region', regionInput.value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
fetch(`${url}?${params}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
this.suggestions = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectSuggestion(suggestion) {
|
|
||||||
this.query = suggestion.name;
|
|
||||||
this.suggestions = [];
|
|
||||||
|
|
||||||
// If this is a form field (not filter), update hidden fields
|
|
||||||
if (!filterParks) {
|
|
||||||
const hiddenField = document.getElementById(`id_${field}`);
|
|
||||||
if (hiddenField) {
|
|
||||||
hiddenField.value = suggestion.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear dependent fields when parent field changes
|
|
||||||
if (field === 'country') {
|
|
||||||
const regionInput = document.getElementById('id_region_name');
|
|
||||||
const cityInput = document.getElementById('id_city_name');
|
|
||||||
const regionHidden = document.getElementById('id_region');
|
|
||||||
const cityHidden = document.getElementById('id_city');
|
|
||||||
|
|
||||||
if (regionInput) regionInput.value = '';
|
|
||||||
if (cityInput) cityInput.value = '';
|
|
||||||
if (regionHidden) regionHidden.value = '';
|
|
||||||
if (cityHidden) cityHidden.value = '';
|
|
||||||
} else if (field === 'region') {
|
|
||||||
const cityInput = document.getElementById('id_city_name');
|
|
||||||
const cityHidden = document.getElementById('id_city');
|
|
||||||
|
|
||||||
if (cityInput) cityInput.value = '';
|
|
||||||
if (cityHidden) cityHidden.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger form submission for filters
|
|
||||||
if (filterParks) {
|
|
||||||
htmx.trigger('#park-filters', 'change');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
// Theme handling
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
|
||||||
const html = document.documentElement;
|
|
||||||
|
|
||||||
// Initialize toggle state based on current theme
|
|
||||||
if (themeToggle) {
|
|
||||||
themeToggle.checked = html.classList.contains('dark');
|
|
||||||
|
|
||||||
// Handle toggle changes
|
|
||||||
themeToggle.addEventListener('change', function() {
|
|
||||||
const isDark = this.checked;
|
|
||||||
html.classList.toggle('dark', isDark);
|
|
||||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for system theme changes
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
mediaQuery.addEventListener('change', (e) => {
|
|
||||||
if (!localStorage.getItem('theme')) {
|
|
||||||
const isDark = e.matches;
|
|
||||||
html.classList.toggle('dark', isDark);
|
|
||||||
themeToggle.checked = isDark;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle search form submission
|
|
||||||
document.addEventListener('submit', (e) => {
|
|
||||||
if (e.target.matches('form[action*="search"]')) {
|
|
||||||
const searchInput = e.target.querySelector('input[name="q"]');
|
|
||||||
if (!searchInput.value.trim()) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mobile menu toggle with transitions
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
|
||||||
const mobileMenu = document.getElementById('mobileMenu');
|
|
||||||
|
|
||||||
if (mobileMenuBtn && mobileMenu) {
|
|
||||||
let isMenuOpen = false;
|
|
||||||
|
|
||||||
const toggleMenu = () => {
|
|
||||||
isMenuOpen = !isMenuOpen;
|
|
||||||
mobileMenu.classList.toggle('show', isMenuOpen);
|
|
||||||
mobileMenuBtn.setAttribute('aria-expanded', isMenuOpen.toString());
|
|
||||||
|
|
||||||
// Update icon
|
|
||||||
const icon = mobileMenuBtn.querySelector('i');
|
|
||||||
icon.classList.remove(isMenuOpen ? 'fa-bars' : 'fa-times');
|
|
||||||
icon.classList.add(isMenuOpen ? 'fa-times' : 'fa-bars');
|
|
||||||
};
|
|
||||||
|
|
||||||
mobileMenuBtn.addEventListener('click', toggleMenu);
|
|
||||||
|
|
||||||
// Close menu when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (isMenuOpen && !mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
|
|
||||||
toggleMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close menu when pressing escape
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (isMenuOpen && e.key === 'Escape') {
|
|
||||||
toggleMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle viewport changes
|
|
||||||
const mediaQuery = window.matchMedia('(min-width: 1024px)');
|
|
||||||
mediaQuery.addEventListener('change', (e) => {
|
|
||||||
if (e.matches && isMenuOpen) {
|
|
||||||
toggleMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// User dropdown toggle
|
|
||||||
const userMenuBtn = document.getElementById('userMenuBtn');
|
|
||||||
const userDropdown = document.getElementById('userDropdown');
|
|
||||||
|
|
||||||
if (userMenuBtn && userDropdown) {
|
|
||||||
userMenuBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
userDropdown.classList.toggle('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!userMenuBtn.contains(e.target) && !userDropdown.contains(e.target)) {
|
|
||||||
userDropdown.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdown when pressing escape
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
userDropdown.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle flash messages
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const alerts = document.querySelectorAll('.alert');
|
|
||||||
alerts.forEach(alert => {
|
|
||||||
setTimeout(() => {
|
|
||||||
alert.style.opacity = '0';
|
|
||||||
setTimeout(() => alert.remove(), 300);
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize tooltips
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const tooltips = document.querySelectorAll('[data-tooltip]');
|
|
||||||
tooltips.forEach(tooltip => {
|
|
||||||
tooltip.addEventListener('mouseenter', (e) => {
|
|
||||||
const text = e.target.getAttribute('data-tooltip');
|
|
||||||
const tooltipEl = document.createElement('div');
|
|
||||||
tooltipEl.className = 'absolute z-50 px-2 py-1 text-sm text-white bg-gray-900 rounded tooltip';
|
|
||||||
tooltipEl.textContent = text;
|
|
||||||
document.body.appendChild(tooltipEl);
|
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect();
|
|
||||||
tooltipEl.style.top = rect.bottom + 5 + 'px';
|
|
||||||
tooltipEl.style.left = rect.left + (rect.width - tooltipEl.offsetWidth) / 2 + 'px';
|
|
||||||
});
|
|
||||||
|
|
||||||
tooltip.addEventListener('mouseleave', () => {
|
|
||||||
const tooltips = document.querySelectorAll('.tooltip');
|
|
||||||
tooltips.forEach(t => t.remove());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Only declare parkMap if it doesn't exist
|
|
||||||
window.parkMap = window.parkMap || null;
|
|
||||||
|
|
||||||
function initParkMap(latitude, longitude, name) {
|
|
||||||
const mapContainer = document.getElementById('park-map');
|
|
||||||
|
|
||||||
// Only initialize if container exists and map hasn't been initialized
|
|
||||||
if (mapContainer && !window.parkMap) {
|
|
||||||
const width = mapContainer.offsetWidth;
|
|
||||||
mapContainer.style.height = width + 'px';
|
|
||||||
|
|
||||||
window.parkMap = L.map('park-map').setView([latitude, longitude], 13);
|
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
attribution: '© OpenStreetMap contributors'
|
|
||||||
}).addTo(window.parkMap);
|
|
||||||
|
|
||||||
L.marker([latitude, longitude])
|
|
||||||
.addTo(window.parkMap)
|
|
||||||
.bindPopup(name);
|
|
||||||
|
|
||||||
// Update map size when window is resized
|
|
||||||
window.addEventListener('resize', function() {
|
|
||||||
const width = mapContainer.offsetWidth;
|
|
||||||
mapContainer.style.height = width + 'px';
|
|
||||||
window.parkMap.invalidateSize();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
Alpine.data('photoDisplay', ({ photos, contentType, objectId, csrfToken, uploadUrl }) => ({
|
|
||||||
photos,
|
|
||||||
fullscreenPhoto: null,
|
|
||||||
uploading: false,
|
|
||||||
uploadProgress: 0,
|
|
||||||
error: null,
|
|
||||||
showSuccess: false,
|
|
||||||
|
|
||||||
showFullscreen(photo) {
|
|
||||||
this.fullscreenPhoto = photo;
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleFileSelect(event) {
|
|
||||||
const files = Array.from(event.target.files);
|
|
||||||
if (!files.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uploading = true;
|
|
||||||
this.uploadProgress = 0;
|
|
||||||
this.error = null;
|
|
||||||
this.showSuccess = false;
|
|
||||||
|
|
||||||
const totalFiles = files.length;
|
|
||||||
let completedFiles = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', file);
|
|
||||||
formData.append('app_label', contentType.split('.')[0]);
|
|
||||||
formData.append('model', contentType.split('.')[1]);
|
|
||||||
formData.append('object_id', objectId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(uploadUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
throw new Error(data.error || 'Upload failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const photo = await response.json();
|
|
||||||
this.photos.push(photo);
|
|
||||||
completedFiles++;
|
|
||||||
this.uploadProgress = (completedFiles / totalFiles) * 100;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message || 'Failed to upload photo. Please try again.';
|
|
||||||
console.error('Upload error:', err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uploading = false;
|
|
||||||
event.target.value = ''; // Reset file input
|
|
||||||
|
|
||||||
if (!this.error) {
|
|
||||||
this.showSuccess = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.showSuccess = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async sharePhoto(photo) {
|
|
||||||
if (navigator.share) {
|
|
||||||
try {
|
|
||||||
await navigator.share({
|
|
||||||
title: photo.caption || 'Shared photo',
|
|
||||||
url: photo.url
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name !== 'AbortError') {
|
|
||||||
console.error('Error sharing:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback: copy URL to clipboard
|
|
||||||
navigator.clipboard.writeText(photo.url)
|
|
||||||
.then(() => alert('Photo URL copied to clipboard!'))
|
|
||||||
.catch(err => console.error('Error copying to clipboard:', err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
function parkSearch() {
|
|
||||||
return {
|
|
||||||
query: '',
|
|
||||||
results: [],
|
|
||||||
loading: false,
|
|
||||||
selectedId: null,
|
|
||||||
|
|
||||||
async search() {
|
|
||||||
if (!this.query.trim()) {
|
|
||||||
this.results = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/parks/suggest_parks/?search=${encodeURIComponent(this.query)}`);
|
|
||||||
const data = await response.json();
|
|
||||||
this.results = data.results;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Search failed:', error);
|
|
||||||
this.results = [];
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.query = '';
|
|
||||||
this.results = [];
|
|
||||||
this.selectedId = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
selectPark(park) {
|
|
||||||
this.query = park.name;
|
|
||||||
this.selectedId = park.id;
|
|
||||||
this.results = [];
|
|
||||||
|
|
||||||
// Trigger filter update
|
|
||||||
document.getElementById('park-filters').dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
const data = document.currentScript.dataset;
|
|
||||||
const isDebug = data.debug === "True";
|
|
||||||
|
|
||||||
if (isDebug) {
|
|
||||||
document.addEventListener("htmx:beforeOnLoad", function (event) {
|
|
||||||
const xhr = event.detail.xhr;
|
|
||||||
if (xhr.status == 500 || xhr.status == 404) {
|
|
||||||
// Tell htmx to stop processing this response
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
document.children[0].innerHTML = xhr.response;
|
|
||||||
|
|
||||||
// Run Django’s inline script
|
|
||||||
// (1, eval) wtf - see https://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript
|
|
||||||
(1, eval)(document.scripts[0].innerText);
|
|
||||||
// Need to directly call Django’s onload function since browser won’t
|
|
||||||
window.onload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Get all alert elements
|
|
||||||
const alerts = document.querySelectorAll('.alert');
|
|
||||||
|
|
||||||
// For each alert
|
|
||||||
alerts.forEach(alert => {
|
|
||||||
// After 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
// Add slideOut animation
|
|
||||||
alert.style.animation = 'slideOut 0.5s ease-out forwards';
|
|
||||||
|
|
||||||
// Remove the alert after animation completes
|
|
||||||
setTimeout(() => {
|
|
||||||
alert.remove();
|
|
||||||
}, 500);
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
5
staticfiles/js/alpine.min.js
vendored
5
staticfiles/js/alpine.min.js
vendored
File diff suppressed because one or more lines are too long
5
staticfiles/js/cdn.min.js
vendored
5
staticfiles/js/cdn.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,262 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Handle edit button clicks
|
|
||||||
document.querySelectorAll('[data-edit-button]').forEach(button => {
|
|
||||||
button.addEventListener('click', function() {
|
|
||||||
const contentId = this.dataset.contentId;
|
|
||||||
const contentType = this.dataset.contentType;
|
|
||||||
const editableFields = document.querySelectorAll(`[data-editable][data-content-id="${contentId}"]`);
|
|
||||||
|
|
||||||
// Toggle edit mode
|
|
||||||
editableFields.forEach(field => {
|
|
||||||
const currentValue = field.textContent.trim();
|
|
||||||
const fieldName = field.dataset.fieldName;
|
|
||||||
const fieldType = field.dataset.fieldType || 'text';
|
|
||||||
|
|
||||||
// Create input field
|
|
||||||
let input;
|
|
||||||
if (fieldType === 'textarea') {
|
|
||||||
input = document.createElement('textarea');
|
|
||||||
input.value = currentValue;
|
|
||||||
input.rows = 4;
|
|
||||||
} else if (fieldType === 'select') {
|
|
||||||
input = document.createElement('select');
|
|
||||||
// Get options from data attribute
|
|
||||||
const options = JSON.parse(field.dataset.options || '[]');
|
|
||||||
options.forEach(option => {
|
|
||||||
const optionEl = document.createElement('option');
|
|
||||||
optionEl.value = option.value;
|
|
||||||
optionEl.textContent = option.label;
|
|
||||||
optionEl.selected = option.value === currentValue;
|
|
||||||
input.appendChild(optionEl);
|
|
||||||
});
|
|
||||||
} else if (fieldType === 'date') {
|
|
||||||
input = document.createElement('input');
|
|
||||||
input.type = 'date';
|
|
||||||
input.value = currentValue;
|
|
||||||
} else if (fieldType === 'number') {
|
|
||||||
input = document.createElement('input');
|
|
||||||
input.type = 'number';
|
|
||||||
input.value = currentValue;
|
|
||||||
if (field.dataset.min) input.min = field.dataset.min;
|
|
||||||
if (field.dataset.max) input.max = field.dataset.max;
|
|
||||||
if (field.dataset.step) input.step = field.dataset.step;
|
|
||||||
} else {
|
|
||||||
input = document.createElement('input');
|
|
||||||
input.type = fieldType;
|
|
||||||
input.value = currentValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.className = 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white';
|
|
||||||
input.dataset.originalValue = currentValue;
|
|
||||||
input.dataset.fieldName = fieldName;
|
|
||||||
|
|
||||||
// Replace content with input
|
|
||||||
field.textContent = '';
|
|
||||||
field.appendChild(input);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show save/cancel buttons
|
|
||||||
const actionButtons = document.createElement('div');
|
|
||||||
actionButtons.className = 'flex gap-2 mt-2';
|
|
||||||
actionButtons.innerHTML = `
|
|
||||||
<button 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" data-save-button>
|
|
||||||
<i class="mr-2 fas fa-save"></i>Save Changes
|
|
||||||
</button>
|
|
||||||
<button 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" data-cancel-button>
|
|
||||||
<i class="mr-2 fas fa-times"></i>Cancel
|
|
||||||
</button>
|
|
||||||
${this.dataset.requireReason ? `
|
|
||||||
<div class="flex-grow">
|
|
||||||
<input type="text" class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="Reason for changes (required)"
|
|
||||||
data-reason-input>
|
|
||||||
<input type="text" class="w-full mt-1 border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="Source (optional)"
|
|
||||||
data-source-input>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const container = editableFields[0].closest('.editable-container');
|
|
||||||
container.appendChild(actionButtons);
|
|
||||||
|
|
||||||
// Hide edit button while editing
|
|
||||||
this.style.display = 'none';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submissions
|
|
||||||
document.querySelectorAll('form[data-submit-type]').forEach(form => {
|
|
||||||
form.addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const submitType = this.dataset.submitType;
|
|
||||||
const formData = new FormData(this);
|
|
||||||
const data = {};
|
|
||||||
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
data[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get CSRF token from meta tag
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
||||||
|
|
||||||
// Submit form
|
|
||||||
fetch(this.action, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
submission_type: submitType,
|
|
||||||
...data
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
showNotification(data.message, 'success');
|
|
||||||
if (data.redirect_url) {
|
|
||||||
window.location.href = data.redirect_url;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showNotification(data.message, 'error');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showNotification('An error occurred while submitting the form.', 'error');
|
|
||||||
console.error('Error:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle save button clicks using event delegation
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (e.target.matches('[data-save-button]')) {
|
|
||||||
const container = e.target.closest('.editable-container');
|
|
||||||
const contentId = container.querySelector('[data-editable]').dataset.contentId;
|
|
||||||
const contentType = container.querySelector('[data-edit-button]').dataset.contentType;
|
|
||||||
const editableFields = container.querySelectorAll('[data-editable]');
|
|
||||||
|
|
||||||
// Collect changes
|
|
||||||
const changes = {};
|
|
||||||
editableFields.forEach(field => {
|
|
||||||
const input = field.querySelector('input, textarea, select');
|
|
||||||
if (input && input.value !== input.dataset.originalValue) {
|
|
||||||
changes[input.dataset.fieldName] = input.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If no changes, just cancel
|
|
||||||
if (Object.keys(changes).length === 0) {
|
|
||||||
cancelEdit(container);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get reason and source if required
|
|
||||||
const reasonInput = container.querySelector('[data-reason-input]');
|
|
||||||
const sourceInput = container.querySelector('[data-source-input]');
|
|
||||||
const reason = reasonInput ? reasonInput.value : '';
|
|
||||||
const source = sourceInput ? sourceInput.value : '';
|
|
||||||
|
|
||||||
// Validate reason if required
|
|
||||||
if (reasonInput && !reason) {
|
|
||||||
alert('Please provide a reason for your changes.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get CSRF token from meta tag
|
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
||||||
|
|
||||||
// Submit changes
|
|
||||||
fetch(window.location.pathname, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
content_type: contentType,
|
|
||||||
content_id: contentId,
|
|
||||||
changes,
|
|
||||||
reason,
|
|
||||||
source
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
if (data.auto_approved) {
|
|
||||||
// Update the display immediately
|
|
||||||
Object.entries(changes).forEach(([field, value]) => {
|
|
||||||
const element = container.querySelector(`[data-editable][data-field-name="${field}"]`);
|
|
||||||
if (element) {
|
|
||||||
element.textContent = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
showNotification(data.message, 'success');
|
|
||||||
if (data.redirect_url) {
|
|
||||||
window.location.href = data.redirect_url;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showNotification(data.message, 'error');
|
|
||||||
}
|
|
||||||
cancelEdit(container);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showNotification('An error occurred while saving changes.', 'error');
|
|
||||||
console.error('Error:', error);
|
|
||||||
cancelEdit(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle cancel button clicks using event delegation
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (e.target.matches('[data-cancel-button]')) {
|
|
||||||
const container = e.target.closest('.editable-container');
|
|
||||||
cancelEdit(container);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function cancelEdit(container) {
|
|
||||||
// Restore original content
|
|
||||||
container.querySelectorAll('[data-editable]').forEach(field => {
|
|
||||||
const input = field.querySelector('input, textarea, select');
|
|
||||||
if (input) {
|
|
||||||
field.textContent = input.dataset.originalValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove action buttons
|
|
||||||
const actionButtons = container.querySelector('.flex.gap-2');
|
|
||||||
if (actionButtons) {
|
|
||||||
actionButtons.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show edit button
|
|
||||||
const editButton = container.querySelector('[data-edit-button]');
|
|
||||||
if (editButton) {
|
|
||||||
editButton.style.display = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showNotification(message, type = 'info') {
|
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.className = `fixed bottom-4 right-4 p-4 rounded-lg shadow-lg text-white ${
|
|
||||||
type === 'success' ? 'bg-green-600 dark:bg-green-500' :
|
|
||||||
type === 'error' ? 'bg-red-600 dark:bg-red-500' :
|
|
||||||
'bg-blue-600 dark:bg-blue-500'
|
|
||||||
}`;
|
|
||||||
notification.textContent = message;
|
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
// Remove after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.remove();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
function locationAutocomplete(field, filterParks = false) {
|
|
||||||
return {
|
|
||||||
query: '',
|
|
||||||
suggestions: [],
|
|
||||||
fetchSuggestions() {
|
|
||||||
let url;
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
q: this.query,
|
|
||||||
filter_parks: filterParks
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (field) {
|
|
||||||
case 'country':
|
|
||||||
url = '/parks/ajax/countries/';
|
|
||||||
break;
|
|
||||||
case 'region':
|
|
||||||
url = '/parks/ajax/regions/';
|
|
||||||
// Add country parameter if we're fetching regions
|
|
||||||
const countryInput = document.getElementById(filterParks ? 'country' : 'id_country_name');
|
|
||||||
if (countryInput && countryInput.value) {
|
|
||||||
params.append('country', countryInput.value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'city':
|
|
||||||
url = '/parks/ajax/cities/';
|
|
||||||
// Add country and region parameters if we're fetching cities
|
|
||||||
const regionInput = document.getElementById(filterParks ? 'region' : 'id_region_name');
|
|
||||||
const cityCountryInput = document.getElementById(filterParks ? 'country' : 'id_country_name');
|
|
||||||
if (regionInput && regionInput.value && cityCountryInput && cityCountryInput.value) {
|
|
||||||
params.append('country', cityCountryInput.value);
|
|
||||||
params.append('region', regionInput.value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
fetch(`${url}?${params}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
this.suggestions = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectSuggestion(suggestion) {
|
|
||||||
this.query = suggestion.name;
|
|
||||||
this.suggestions = [];
|
|
||||||
|
|
||||||
// If this is a form field (not filter), update hidden fields
|
|
||||||
if (!filterParks) {
|
|
||||||
const hiddenField = document.getElementById(`id_${field}`);
|
|
||||||
if (hiddenField) {
|
|
||||||
hiddenField.value = suggestion.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear dependent fields when parent field changes
|
|
||||||
if (field === 'country') {
|
|
||||||
const regionInput = document.getElementById('id_region_name');
|
|
||||||
const cityInput = document.getElementById('id_city_name');
|
|
||||||
const regionHidden = document.getElementById('id_region');
|
|
||||||
const cityHidden = document.getElementById('id_city');
|
|
||||||
|
|
||||||
if (regionInput) regionInput.value = '';
|
|
||||||
if (cityInput) cityInput.value = '';
|
|
||||||
if (regionHidden) regionHidden.value = '';
|
|
||||||
if (cityHidden) cityHidden.value = '';
|
|
||||||
} else if (field === 'region') {
|
|
||||||
const cityInput = document.getElementById('id_city_name');
|
|
||||||
const cityHidden = document.getElementById('id_city');
|
|
||||||
|
|
||||||
if (cityInput) cityInput.value = '';
|
|
||||||
if (cityHidden) cityHidden.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger form submission for filters
|
|
||||||
if (filterParks) {
|
|
||||||
htmx.trigger('#park-filters', 'change');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// Initialize dark mode from localStorage
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// Check if dark mode was previously enabled
|
|
||||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
|
||||||
if (darkMode) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle search form submission
|
|
||||||
document.addEventListener('submit', (e) => {
|
|
||||||
if (e.target.matches('form[action*="search"]')) {
|
|
||||||
const searchInput = e.target.querySelector('input[name="q"]');
|
|
||||||
if (!searchInput.value.trim()) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close mobile menu when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
const mobileMenu = document.querySelector('[x-show="mobileMenuOpen"]');
|
|
||||||
const menuButton = document.querySelector('[x-on\\:click="mobileMenuOpen = !mobileMenuOpen"]');
|
|
||||||
|
|
||||||
if (mobileMenu && menuButton && !mobileMenu.contains(e.target) && !menuButton.contains(e.target)) {
|
|
||||||
const alpineData = mobileMenu._x_dataStack && mobileMenu._x_dataStack[0];
|
|
||||||
if (alpineData && alpineData.mobileMenuOpen) {
|
|
||||||
alpineData.mobileMenuOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle flash messages
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const alerts = document.querySelectorAll('.alert');
|
|
||||||
alerts.forEach(alert => {
|
|
||||||
setTimeout(() => {
|
|
||||||
alert.style.opacity = '0';
|
|
||||||
setTimeout(() => alert.remove(), 300);
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize tooltips
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const tooltips = document.querySelectorAll('[data-tooltip]');
|
|
||||||
tooltips.forEach(tooltip => {
|
|
||||||
tooltip.addEventListener('mouseenter', (e) => {
|
|
||||||
const text = e.target.getAttribute('data-tooltip');
|
|
||||||
const tooltipEl = document.createElement('div');
|
|
||||||
tooltipEl.className = 'tooltip bg-gray-900 text-white px-2 py-1 rounded text-sm absolute z-50';
|
|
||||||
tooltipEl.textContent = text;
|
|
||||||
document.body.appendChild(tooltipEl);
|
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect();
|
|
||||||
tooltipEl.style.top = rect.bottom + 5 + 'px';
|
|
||||||
tooltipEl.style.left = rect.left + (rect.width - tooltipEl.offsetWidth) / 2 + 'px';
|
|
||||||
});
|
|
||||||
|
|
||||||
tooltip.addEventListener('mouseleave', () => {
|
|
||||||
const tooltips = document.querySelectorAll('.tooltip');
|
|
||||||
tooltips.forEach(t => t.remove());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle dropdown menus
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
const dropdowns = document.querySelectorAll('[x-show]');
|
|
||||||
dropdowns.forEach(dropdown => {
|
|
||||||
if (!dropdown.contains(e.target) &&
|
|
||||||
!e.target.matches('[x-on\\:click*="open = !open"]')) {
|
|
||||||
const alpineData = dropdown._x_dataStack && dropdown._x_dataStack[0];
|
|
||||||
if (alpineData && alpineData.open) {
|
|
||||||
alpineData.open = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
let parkMap = null;
|
|
||||||
|
|
||||||
function initParkMap(latitude, longitude, name) {
|
|
||||||
const mapContainer = document.getElementById('park-map');
|
|
||||||
|
|
||||||
// Only initialize if container exists and map hasn't been initialized
|
|
||||||
if (mapContainer && !parkMap) {
|
|
||||||
const width = mapContainer.offsetWidth;
|
|
||||||
mapContainer.style.height = width + 'px';
|
|
||||||
|
|
||||||
parkMap = L.map('park-map').setView([latitude, longitude], 13);
|
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
attribution: '© OpenStreetMap contributors'
|
|
||||||
}).addTo(parkMap);
|
|
||||||
|
|
||||||
L.marker([latitude, longitude])
|
|
||||||
.addTo(parkMap)
|
|
||||||
.bindPopup(name);
|
|
||||||
|
|
||||||
// Update map size when window is resized
|
|
||||||
window.addEventListener('resize', function() {
|
|
||||||
const width = mapContainer.offsetWidth;
|
|
||||||
mapContainer.style.height = width + 'px';
|
|
||||||
parkMap.invalidateSize();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
Alpine.data('photoDisplay', ({ photos, contentType, objectId, csrfToken, uploadUrl }) => ({
|
|
||||||
photos,
|
|
||||||
fullscreenPhoto: null,
|
|
||||||
uploading: false,
|
|
||||||
uploadProgress: 0,
|
|
||||||
error: null,
|
|
||||||
showSuccess: false,
|
|
||||||
|
|
||||||
showFullscreen(photo) {
|
|
||||||
this.fullscreenPhoto = photo;
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleFileSelect(event) {
|
|
||||||
const files = Array.from(event.target.files);
|
|
||||||
if (!files.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uploading = true;
|
|
||||||
this.uploadProgress = 0;
|
|
||||||
this.error = null;
|
|
||||||
this.showSuccess = false;
|
|
||||||
|
|
||||||
const totalFiles = files.length;
|
|
||||||
let completedFiles = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', file);
|
|
||||||
formData.append('app_label', contentType.split('.')[0]);
|
|
||||||
formData.append('model', contentType.split('.')[1]);
|
|
||||||
formData.append('object_id', objectId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(uploadUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
throw new Error(data.error || 'Upload failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const photo = await response.json();
|
|
||||||
this.photos.push(photo);
|
|
||||||
completedFiles++;
|
|
||||||
this.uploadProgress = (completedFiles / totalFiles) * 100;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message || 'Failed to upload photo. Please try again.';
|
|
||||||
console.error('Upload error:', err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uploading = false;
|
|
||||||
event.target.value = ''; // Reset file input
|
|
||||||
|
|
||||||
if (!this.error) {
|
|
||||||
this.showSuccess = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.showSuccess = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async sharePhoto(photo) {
|
|
||||||
if (navigator.share) {
|
|
||||||
try {
|
|
||||||
await navigator.share({
|
|
||||||
title: photo.caption || 'Shared photo',
|
|
||||||
url: photo.url
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name !== 'AbortError') {
|
|
||||||
console.error('Error sharing:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback: copy URL to clipboard
|
|
||||||
navigator.clipboard.writeText(photo.url)
|
|
||||||
.then(() => alert('Photo URL copied to clipboard!'))
|
|
||||||
.catch(err => console.error('Error copying to clipboard:', err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user