At this point it also appears that I'm correctly capturing the GPS location as both PostGIS data and as an H3 cell.
300 lines
9.8 KiB
HTML
300 lines
9.8 KiB
HTML
{{template "base.html" .}}
|
|
|
|
{{define "title"}}Dash{{end}}
|
|
{{define "extraheader"}}
|
|
<style>
|
|
.district-logo {
|
|
max-height: 60px;
|
|
width: auto;
|
|
}
|
|
|
|
.photo-upload-area {
|
|
border: 2px dashed #ccc;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
background-color: #f9f9f9;
|
|
}
|
|
|
|
.photo-preview {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.photo-preview img {
|
|
width: 80px;
|
|
height: 80px;
|
|
object-fit: cover;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.submit-btn {
|
|
padding: 15px 0;
|
|
font-size: 1.25rem;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.location-info {
|
|
background-color: #e9f5ff;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin-bottom: 20px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.header-title {
|
|
font-size: 1.5rem;
|
|
}
|
|
}
|
|
</style>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Elements
|
|
const form = document.getElementById('mosquitoReportForm');
|
|
const photoInput = document.getElementById('photos');
|
|
const photoPreviewContainer = document.getElementById('photoPreviewContainer');
|
|
const locationStatus = document.getElementById('locationStatus');
|
|
const latitudeInput = document.getElementById('latitude');
|
|
const longitudeInput = document.getElementById('longitude');
|
|
const submitButton = document.getElementById('submitButton');
|
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
|
|
// Get current location
|
|
requestLocation();
|
|
|
|
// Handle photo selection
|
|
photoInput.addEventListener('change', handlePhotoSelection);
|
|
|
|
// Handle form submission
|
|
form.addEventListener('submit', handleFormSubmission);
|
|
|
|
/**
|
|
* Request user's geolocation
|
|
*/
|
|
function requestLocation() {
|
|
if (navigator.geolocation) {
|
|
locationStatus.textContent = "Requesting your location...";
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
// Success callback
|
|
function(position) {
|
|
locationStatus.textContent = "Location successfully added";
|
|
locationStatus.classList.add('text-success');
|
|
|
|
// Store location in hidden fields
|
|
latitudeInput.value = position.coords.latitude;
|
|
longitudeInput.value = position.coords.longitude;
|
|
},
|
|
// Error callback
|
|
function(error) {
|
|
let errorMessage = "Unable to get your location";
|
|
switch(error.code) {
|
|
case error.PERMISSION_DENIED:
|
|
errorMessage = "Location access denied. Please enable location services.";
|
|
break;
|
|
case error.POSITION_UNAVAILABLE:
|
|
errorMessage = "Location information unavailable.";
|
|
break;
|
|
case error.TIMEOUT:
|
|
errorMessage = "Location request timed out.";
|
|
break;
|
|
}
|
|
locationStatus.textContent = errorMessage;
|
|
locationStatus.classList.add('text-danger');
|
|
},
|
|
// Options
|
|
{
|
|
enableHighAccuracy: true,
|
|
timeout: 10000,
|
|
maximumAge: 0
|
|
}
|
|
);
|
|
} else {
|
|
locationStatus.textContent = "Geolocation is not supported by your browser";
|
|
locationStatus.classList.add('text-danger');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle photo selection and preview
|
|
*/
|
|
function handlePhotoSelection() {
|
|
// Clear previous previews
|
|
photoPreviewContainer.innerHTML = '';
|
|
|
|
// Check if files were selected
|
|
if (photoInput.files && photoInput.files.length > 0) {
|
|
// Loop through selected files
|
|
Array.from(photoInput.files).forEach((file, index) => {
|
|
if (!file.type.match('image.*')) {
|
|
return; // Skip non-image files
|
|
}
|
|
|
|
// Create preview container
|
|
const previewContainer = document.createElement('div');
|
|
previewContainer.className = 'position-relative m-1';
|
|
|
|
// Create image preview
|
|
const img = document.createElement('img');
|
|
img.className = 'img-thumbnail';
|
|
img.style.width = '100px';
|
|
img.style.height = '100px';
|
|
img.style.objectFit = 'cover';
|
|
|
|
// Read file and set preview
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
img.src = e.target.result;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
// Create remove button
|
|
const removeBtn = document.createElement('button');
|
|
removeBtn.type = 'button';
|
|
removeBtn.className = 'btn btn-sm btn-danger position-absolute top-0 end-0';
|
|
removeBtn.innerHTML = '×';
|
|
removeBtn.style.fontSize = '10px';
|
|
removeBtn.style.padding = '0 5px';
|
|
|
|
// Handle remove button click
|
|
removeBtn.addEventListener('click', function() {
|
|
// Create a new FileList without this file
|
|
// Since FileList is immutable, we need to reset the input
|
|
// This is a bit tricky and requires recreating the input
|
|
previewContainer.remove();
|
|
|
|
// If this was the last image, clear the input entirely
|
|
if (photoPreviewContainer.children.length === 0) {
|
|
photoInput.value = '';
|
|
}
|
|
// Note: Unfortunately, selectively removing files from a FileList isn't straightforward
|
|
// In a real implementation, we might track selected files in an array and recreate the input
|
|
});
|
|
|
|
// Add elements to the preview container
|
|
previewContainer.appendChild(img);
|
|
previewContainer.appendChild(removeBtn);
|
|
photoPreviewContainer.appendChild(previewContainer);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle form submission
|
|
*/
|
|
function handleFormSubmission(event) {
|
|
event.preventDefault();
|
|
|
|
// Show loading overlay
|
|
loadingOverlay.classList.remove('d-none');
|
|
|
|
// Disable submit button to prevent double submission
|
|
submitButton.disabled = true;
|
|
|
|
// Create FormData object
|
|
const formData = new FormData(form);
|
|
|
|
// Send AJAX request
|
|
fetch(form.action, {
|
|
method: 'POST',
|
|
body: formData,
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
// Navigate to the URL the server specified
|
|
window.location.href = response.url;
|
|
return;
|
|
}
|
|
console.error("not ok server response", response);
|
|
throw new Error("Server error " + response.status);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('There was a problem submitting your report. Please try again. If this happens a few times, please let us know.');
|
|
|
|
// Re-enable submit button
|
|
submitButton.disabled = false;
|
|
|
|
// Hide loading overlay
|
|
loadingOverlay.classList.add('d-none');
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{{end}}
|
|
{{define "content"}}
|
|
<!-- Main Content -->
|
|
<main class="container mb-5">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body p-4">
|
|
<h2 class="card-title text-center mb-4">Quick Mosquito Report</h2>
|
|
|
|
<!-- Form -->
|
|
<form id="mosquitoReportForm" action="/quick-submit" method="POST" enctype="multipart/form-data">
|
|
<!-- Location Automatic Collection Note -->
|
|
<div class="location-info d-flex align-items-center mb-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-geo-alt me-2" viewBox="0 0 16 16">
|
|
<path d="M12.166 8.94c-.524 1.062-1.234 2.12-1.96 3.07A31.493 31.493 0 0 1 8 14.58a31.481 31.481 0 0 1-2.206-2.57c-.726-.95-1.436-2.008-1.96-3.07C3.304 7.867 3 6.862 3 6a5 5 0 0 1 10 0c0 .862-.305 1.867-.834 2.94zM8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10z"/>
|
|
<path d="M8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 1a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
|
</svg>
|
|
<span id="locationStatus">Requesting your location...</span>
|
|
<!-- Hidden fields for location data -->
|
|
<input type="hidden" id="latitude" name="latitude">
|
|
<input type="hidden" id="longitude" name="longitude">
|
|
</div>
|
|
|
|
<!-- Photo Upload -->
|
|
<div class="mb-4">
|
|
<label for="photos" class="form-label fw-bold">Photos (Optional)</label>
|
|
<div class="photo-upload-area">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-camera mb-2" viewBox="0 0 16 16">
|
|
<path d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1v6zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4H2z"/>
|
|
<path d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"/>
|
|
</svg>
|
|
<div>
|
|
<input type="file" id="photos" name="photos" class="d-none" accept="image/*" multiple>
|
|
<button type="button" class="btn btn-outline-primary mb-2" onclick="document.getElementById('photos').click()">Add Photos</button>
|
|
</div>
|
|
<small class="d-block text-muted">Take pictures of the mosquito problem area</small>
|
|
|
|
<!-- Photo Preview Area -->
|
|
<div id="photoPreviewContainer" class="photo-preview mt-3 d-flex flex-wrap">
|
|
<!-- Image previews will be added here by JavaScript -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comments -->
|
|
<div class="mb-4">
|
|
<label for="comments" class="form-label fw-bold">Comments</label>
|
|
<textarea class="form-control" id="comments" name="comments" rows="4" placeholder="Describe the mosquito issue (e.g., standing water, high mosquito activity, time of day they're most active)"></textarea>
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<button type="submit" class="btn btn-success w-100 submit-btn mt-4" id="submitButton">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-send-fill me-2" viewBox="0 0 16 16">
|
|
<path d="M15.964.686a.5.5 0 0 0-.65-.65L.767 5.855H.766l-.452.18a.5.5 0 0 0-.082.887l.41.26.001.002 4.995 3.178 3.178 4.995.002.002.26.41a.5.5 0 0 0 .886-.083l6-15Zm-1.833 1.89L6.637 10.07l-.215-.338a.5.5 0 0 0-.154-.154l-.338-.215 7.494-7.494 1.178-.471-.47 1.178Z"/>
|
|
</svg>
|
|
Submit Report
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Loading Indicator Overlay (Initially hidden) -->
|
|
<div id="loadingOverlay" class="position-fixed top-0 start-0 w-100 h-100 d-none" style="background-color: rgba(0,0,0,0.5); z-index: 1050;">
|
|
<div class="position-absolute top-50 start-50 translate-middle text-white text-center">
|
|
<div class="spinner-border" role="status"></div>
|
|
<p class="mt-2">Submitting your report...</p>
|
|
</div>
|
|
</div>
|
|
{{end}}
|