mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 14:31:18 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
304 lines
14 KiB
HTML
304 lines
14 KiB
HTML
{% 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 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>
|
|
|
|
<form method="post" enctype="multipart/form-data" class="space-y-6" role="form" aria-label="{% if is_edit %}Edit park{% else %}Create new park{% endif %}" x-data="parkForm()">
|
|
{% csrf_token %}
|
|
|
|
{# Basic Information #}
|
|
<fieldset class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<legend class="mb-4 text-xl font-semibold">Basic Information</legend>
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div class="col-span-2">
|
|
{% include 'forms/partials/form_field.html' with field=form.name label="Name" %}
|
|
</div>
|
|
<div class="col-span-2">
|
|
{% include 'forms/partials/form_field.html' with field=form.description label="Description" %}
|
|
</div>
|
|
<div>
|
|
{% include 'forms/partials/form_field.html' with field=form.owner label="Owner/Operator" %}
|
|
</div>
|
|
<div>
|
|
{% include 'forms/partials/form_field.html' with field=form.status label="Status" %}
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
{# Location #}
|
|
<fieldset class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<legend class="mb-4 text-xl font-semibold">Location</legend>
|
|
{% include "parks/partials/location_widget.html" %}
|
|
</fieldset>
|
|
|
|
{# Photos #}
|
|
<fieldset class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<legend class="mb-4 text-xl font-semibold">Photos</legend>
|
|
|
|
{# 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 }}')"
|
|
aria-label="Remove photo">
|
|
<i class="fas fa-times" aria-hidden="true"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Photo Upload #}
|
|
<div class="space-y-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" id="photo-upload-label">
|
|
Add Photos
|
|
</label>
|
|
<div class="relative">
|
|
<input type="file"
|
|
multiple
|
|
accept="image/*"
|
|
class="hidden"
|
|
x-ref="fileInput"
|
|
@change="handleFileSelect"
|
|
aria-labelledby="photo-upload-label">
|
|
<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" aria-hidden="true"></i>
|
|
Click to upload photos
|
|
</span>
|
|
<span x-show="previews.length">
|
|
<i class="mr-2 fas fa-plus" aria-hidden="true"></i>
|
|
Add more photos
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
{# Photo Previews #}
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3" role="list" aria-label="Photo previews">
|
|
<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"
|
|
role="listitem"
|
|
:class="{
|
|
'uploading': preview.uploading,
|
|
'error': preview.error,
|
|
'success': preview.uploaded
|
|
}">
|
|
<img :src="preview.url"
|
|
class="object-cover w-full h-full"
|
|
alt="Preview image">
|
|
<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)"
|
|
aria-label="Remove preview">
|
|
<i class="fas fa-times" aria-hidden="true"></i>
|
|
</button>
|
|
</div>
|
|
<div x-show="preview.uploading"
|
|
class="absolute inset-0 flex items-center justify-center bg-black/50">
|
|
<div class="w-16 h-16 border-4 border-blue-500 rounded-full animate-spin border-t-transparent" role="status" aria-label="Uploading"></div>
|
|
</div>
|
|
<div x-show="preview.error"
|
|
class="absolute bottom-0 left-0 right-0 p-2 text-sm text-white bg-red-500" role="alert">
|
|
Upload failed
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
{# Additional Details #}
|
|
<fieldset class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<legend class="mb-4 text-xl font-semibold">Additional Details</legend>
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div>
|
|
{% include 'forms/partials/form_field.html' with field=form.opening_date label="Opening Date" %}
|
|
</div>
|
|
<div>
|
|
{% include 'forms/partials/form_field.html' with field=form.closing_date label="Closing Date" %}
|
|
</div>
|
|
<div>
|
|
{% include 'forms/partials/form_field.html' with field=form.operating_season label="Operating Season" %}
|
|
</div>
|
|
<div>
|
|
{% include 'forms/partials/form_field.html' with field=form.size_acres label="Size (acres)" %}
|
|
</div>
|
|
<div class="col-span-2">
|
|
{% include 'forms/partials/form_field.html' with field=form.website label="Website" %}
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
{# Submission Details #}
|
|
<fieldset class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<legend class="mb-4 text-xl font-semibold">Submission Details</legend>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label for="id_reason" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Reason for Changes
|
|
</label>
|
|
<textarea name="reason" id="id_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"
|
|
aria-describedby="reason-help"></textarea>
|
|
<p id="reason-help" class="mt-1 text-xs text-gray-500 dark:text-gray-400">Optional: Provide context for reviewers</p>
|
|
</div>
|
|
<div>
|
|
<label for="id_source" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Source
|
|
</label>
|
|
<input type="text" name="source" id="id_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?"
|
|
aria-describedby="source-help">
|
|
<p id="source-help" class="mt-1 text-xs text-gray-500 dark:text-gray-400">Optional: Link or reference to your source</p>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
{# 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>
|
|
</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 %}
|