okay fine

This commit is contained in:
pacnpal
2024-11-03 17:47:26 +00:00
parent 01c6004a79
commit 27eb239e97
10020 changed files with 1935769 additions and 2364 deletions

View File

@@ -1,241 +1,321 @@
{% extends 'base/base.html' %} {% load static %} {% block title %}{% if is_edit %}Edit {{ object.name }}{% else %}Add Park{% endif %} - ThrillWiki{% endblock %}
{% 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 mx-auto">
<div class="max-w-3xl mx-auto">
<!-- Park Form -->
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h1 class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">
{% if is_edit %}Edit {{ object.name }}{% else %}Add Park{% endif %}
</h1>
<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>
{% if form.errors %}
<div class="p-4 mb-6 text-red-700 bg-red-100 border border-red-400 rounded-lg dark:bg-red-900 dark:text-red-100 dark:border-red-700">
<p class="font-medium">Please correct the following errors:</p>
<ul class="ml-4 list-disc">
{% for field in form %} {% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %} {% endfor %} {% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post" enctype="multipart/form-data" class="space-y-6" x-data="parkForm()">
{% csrf_token %}
<form method="post" class="space-y-6" id="park-form">
{% csrf_token %}
{# Basic Information #}
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold">Basic Information</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="col-span-2">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name
</label>
{{ form.name }}
</div>
<div class="col-span-2">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Description
</label>
{{ form.description }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Owner/Operator
</label>
{{ form.owner }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Status
</label>
{{ form.status }}
</div>
</div>
</div>
<!-- Hidden fields -->
{{ form.country }} {{ form.region }} {{ form.city }}
{# Location #}
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold">Location</h2>
{% include "parks/partials/location_widget.html" %}
</div>
<!-- Name field -->
<div>
<label for="{{ form.name.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Name *
</label>
<div>{{ form.name }}</div>
{% if form.name.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form.name.errors }}
</div>
{% endif %}
</div>
{# Photos #}
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold">Photos</h2>
{# 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 }}')">
<i class="fas fa-times"></i>
</button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Location fields -->
<div x-data="locationAutocomplete('country', false)" class="relative">
<label
for="{{ form.country_name.id_for_label }}"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
Country *
</label>
<input
type="text"
id="id_country_name"
name="country_name"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select country..."
value="{{ form.country_name.value|default:'' }}"
autocomplete="off"
/>
<!-- Suggestions Dropdown -->
<ul
x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60"
>
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li
@click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600"
></li>
</template>
</ul>
</div>
{# Photo Upload #}
<div class="space-y-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Add Photos
</label>
<div class="relative">
<input type="file"
multiple
accept="image/*"
class="hidden"
x-ref="fileInput"
@change="handleFileSelect">
<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"></i>
Click to upload photos
</span>
<span x-show="previews.length">
<i class="mr-2 fas fa-plus"></i>
Add more photos
</span>
</button>
</div>
<div x-data="locationAutocomplete('region', false)" class="relative">
<label
for="{{ form.region_name.id_for_label }}"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
Region/State
</label>
<input
type="text"
id="id_region_name"
name="region_name"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select region/state..."
value="{{ form.region_name.value|default:'' }}"
autocomplete="off"
/>
<!-- Suggestions Dropdown -->
<ul
x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60"
>
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li
@click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600"
></li>
</template>
</ul>
</div>
{# Photo Previews #}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<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"
:class="{
'uploading': preview.uploading,
'error': preview.error,
'success': preview.uploaded
}">
<img :src="preview.url"
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="removePreview(index)">
<i class="fas fa-times"></i>
</button>
</div>
<div x-show="preview.uploading"
class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div class="w-16 h-16 border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
</div>
<div x-show="preview.error"
class="absolute bottom-0 left-0 right-0 p-2 text-sm text-white bg-red-500">
Upload failed
</div>
</div>
</template>
</div>
</div>
</div>
<div x-data="locationAutocomplete('city', false)" class="relative">
<label
for="{{ form.city_name.id_for_label }}"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
City
</label>
<input
type="text"
id="id_city_name"
name="city_name"
x-model="query"
@input.debounce.300ms="fetchSuggestions()"
@focus="fetchSuggestions()"
@click.away="suggestions = []"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Select city..."
value="{{ form.city_name.value|default:'' }}"
autocomplete="off"
/>
<!-- Suggestions Dropdown -->
<ul
x-show="suggestions.length > 0"
x-cloak
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60"
>
<template x-for="suggestion in suggestions" :key="suggestion.id">
<li
@click="selectSuggestion(suggestion)"
x-text="suggestion.name"
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600"
></li>
</template>
</ul>
</div>
{# Additional Details #}
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold">Additional Details</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Opening Date
</label>
{{ form.opening_date }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Closing Date
</label>
{{ form.closing_date }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Operating Season
</label>
{{ form.operating_season }}
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Size (acres)
</label>
{{ form.size_acres }}
</div>
<div class="col-span-2">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Website
</label>
{{ form.website }}
</div>
</div>
</div>
<!-- Other fields -->
{% for field in form %} {% if field.name not in 'name,country,region,city,country_name,region_name,city_name' %}
<div>
<label
for="{{ field.id_for_label }}"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ field.label }}{% if field.field.required %} *{% endif %}
</label>
<div>{{ field }}</div>
{% if field.help_text %}
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ field.help_text }}
</p>
{% endif %} {% if field.errors %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ field.errors }}
</div>
{% endif %}
</div>
{% endif %} {% endfor %} {% if not user.role == 'MODERATOR' and not user.role == 'ADMIN' and not user.role == 'SUPERUSER' %}
<div
class="p-4 mb-4 text-blue-700 bg-blue-100 border border-blue-400 rounded-lg dark:bg-blue-900 dark:text-blue-100 dark:border-blue-700"
>
<p>
Your submission will be reviewed by a moderator before being
published.
</p>
</div>
<div class="space-y-4">
<div>
<label
for="reason"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
Reason for {% if is_edit %}Edit{% else %}Addition{% endif %} *
</label>
<textarea
name="reason"
id="reason"
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="3"
required
placeholder="Please explain why you're {% if is_edit %}editing{% else %}adding{% endif %} this park and provide any relevant details."
></textarea>
</div>
<div>
<label
for="source"
class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
Source (Optional)
</label>
<input
type="text"
name="source"
id="source"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Link to official website, news article, or other source"
/>
</div>
</div>
{% endif %}
{# Submission Details #}
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold">Submission Details</h2>
<div class="space-y-4">
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Reason for Changes
</label>
<textarea name="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"></textarea>
</div>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Source
</label>
<input type="text" name="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?">
</div>
</div>
</div>
<div class="flex justify-end space-x-4">
<a
href="{% if is_edit %}{% url 'parks:park_detail' slug=object.slug %}{% else %}{% url 'parks:park_list' %}{% endif %}"
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"
>
Cancel
</a>
<button
type="submit"
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"
>
{% if is_edit %}Save Changes{% else %}Submit{% endif %}
</button>
</div>
</form>
{# 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>
<!-- Photos Section (only shown on edit) -->
{% if is_edit %}
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800" id="photos">
{% include "media/partials/photo_manager.html" with photos=object.photos.all content_type="parks.park" object_id=object.id %}
</div>
{% endif %}
</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 %}