mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:11:09 -05:00
fixed the damn discord button
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
125
parks/forms.py
Normal file
125
parks/forms.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
from .models import Park
|
||||
from cities_light.models import Country, Region, City
|
||||
|
||||
class ParkForm(forms.ModelForm):
|
||||
# Hidden fields for actual model relations
|
||||
country = forms.ModelChoiceField(queryset=Country.objects.all(), required=True, widget=forms.HiddenInput())
|
||||
region = forms.ModelChoiceField(queryset=Region.objects.all(), required=False, widget=forms.HiddenInput())
|
||||
city = forms.ModelChoiceField(queryset=City.objects.all(), required=False, widget=forms.HiddenInput())
|
||||
|
||||
# Visible fields for Awesomplete
|
||||
country_name = forms.CharField(
|
||||
label="Country",
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'placeholder': 'Start typing a country name...',
|
||||
})
|
||||
)
|
||||
region_name = forms.CharField(
|
||||
label="Region/State",
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'placeholder': 'Start typing a region/state name...',
|
||||
})
|
||||
)
|
||||
city_name = forms.CharField(
|
||||
label="City",
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'placeholder': 'Start typing a city name...',
|
||||
})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Park
|
||||
fields = ['name', 'country', 'region', 'city', 'description', 'owner', 'status',
|
||||
'opening_date', 'closing_date', 'operating_season', 'size_acres', 'website']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'description': forms.Textarea(attrs={
|
||||
'rows': 4,
|
||||
'class': 'w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'owner': forms.Select(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'status': forms.Select(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'opening_date': forms.DateInput(attrs={
|
||||
'type': 'date',
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'closing_date': forms.DateInput(attrs={
|
||||
'type': 'date',
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'operating_season': forms.TextInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'placeholder': 'e.g., Year-round, Summer only, etc.'
|
||||
}),
|
||||
'size_acres': forms.NumberInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'step': '0.01',
|
||||
'min': '0'
|
||||
}),
|
||||
'website': forms.URLInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'placeholder': 'https://example.com'
|
||||
}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
instance = kwargs.get('instance')
|
||||
if instance:
|
||||
if instance.country:
|
||||
self.fields['country_name'].initial = instance.country.name
|
||||
self.fields['country'].initial = instance.country
|
||||
if instance.region:
|
||||
self.fields['region_name'].initial = instance.region.name
|
||||
self.fields['region'].initial = instance.region
|
||||
if instance.city:
|
||||
self.fields['city_name'].initial = instance.city.name
|
||||
self.fields['city'].initial = instance.city
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
country_name = cleaned_data.get('country_name')
|
||||
region_name = cleaned_data.get('region_name')
|
||||
city_name = cleaned_data.get('city_name')
|
||||
|
||||
if country_name:
|
||||
try:
|
||||
country = Country.objects.get(name__iexact=country_name)
|
||||
cleaned_data['country'] = country
|
||||
except Country.DoesNotExist:
|
||||
self.add_error('country_name', 'Invalid country name')
|
||||
|
||||
if region_name and cleaned_data.get('country'):
|
||||
try:
|
||||
region = Region.objects.get(
|
||||
name__iexact=region_name,
|
||||
country=cleaned_data['country']
|
||||
)
|
||||
cleaned_data['region'] = region
|
||||
except Region.DoesNotExist:
|
||||
self.add_error('region_name', 'Invalid region name for selected country')
|
||||
|
||||
if city_name and cleaned_data.get('region'):
|
||||
try:
|
||||
city = City.objects.get(
|
||||
name__iexact=city_name,
|
||||
region=cleaned_data['region']
|
||||
)
|
||||
cleaned_data['city'] = city
|
||||
except City.DoesNotExist:
|
||||
self.add_error('city_name', 'Invalid city name for selected region')
|
||||
|
||||
return cleaned_data
|
||||
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-30 23:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0003_alter_historicalpark_status_alter_park_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="historicalpark",
|
||||
name="city",
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="historicalpark",
|
||||
name="state",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="State/Province/Region", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="park",
|
||||
name="city",
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="park",
|
||||
name="state",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="State/Province/Region", max_length=255
|
||||
),
|
||||
),
|
||||
]
|
||||
23
parks/migrations/0005_update_country_field_length.py
Normal file
23
parks/migrations/0005_update_country_field_length.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-30 23:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0004_historicalpark_city_historicalpark_state_park_city_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="historicalpark",
|
||||
name="country",
|
||||
field=models.CharField(help_text="Country name", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="country",
|
||||
field=models.CharField(help_text="Country name", max_length=255),
|
||||
),
|
||||
]
|
||||
125
parks/migrations/0006_update_location_fields_to_cities_light.py
Normal file
125
parks/migrations/0006_update_location_fields_to_cities_light.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
def forwards_func(apps, schema_editor):
|
||||
# Get the historical models
|
||||
Park = apps.get_model("parks", "Park")
|
||||
Country = apps.get_model("cities_light", "Country")
|
||||
Region = apps.get_model("cities_light", "Region")
|
||||
City = apps.get_model("cities_light", "City")
|
||||
|
||||
# Create default country for existing parks
|
||||
default_country, _ = Country.objects.get_or_create(
|
||||
name='Unknown',
|
||||
name_ascii='Unknown',
|
||||
slug='unknown',
|
||||
code2='XX'
|
||||
)
|
||||
|
||||
# Store old values
|
||||
parks_data = []
|
||||
for park in Park.objects.all():
|
||||
parks_data.append({
|
||||
'id': park.id,
|
||||
'old_country': park.country,
|
||||
'old_state': park.state,
|
||||
'location': park.location
|
||||
})
|
||||
|
||||
# Remove old fields first
|
||||
Park._meta.get_field('country').null = True
|
||||
Park._meta.get_field('state').null = True
|
||||
Park.objects.all().update(country=None, state=None)
|
||||
|
||||
# Now update with new values
|
||||
for data in parks_data:
|
||||
park = Park.objects.get(id=data['id'])
|
||||
park.country_id = default_country.id
|
||||
park.save()
|
||||
|
||||
def reverse_func(apps, schema_editor):
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cities_light', '0011_alter_city_country_alter_city_region_and_more'),
|
||||
('parks', '0005_update_country_field_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# First make the fields nullable
|
||||
migrations.AlterField(
|
||||
model_name='park',
|
||||
name='country',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalpark',
|
||||
name='country',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='park',
|
||||
name='state',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalpark',
|
||||
name='state',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
|
||||
# Run the data migration
|
||||
migrations.RunPython(forwards_func, reverse_func),
|
||||
|
||||
# Remove old fields
|
||||
migrations.RemoveField(
|
||||
model_name='park',
|
||||
name='state',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalpark',
|
||||
name='state',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='park',
|
||||
name='country',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalpark',
|
||||
name='country',
|
||||
),
|
||||
|
||||
# Add new fields
|
||||
migrations.AddField(
|
||||
model_name='park',
|
||||
name='country',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='cities_light.country'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='park',
|
||||
name='region',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cities_light.region'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='park',
|
||||
name='city',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cities_light.city'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalpark',
|
||||
name='country',
|
||||
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cities_light.country'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalpark',
|
||||
name='region',
|
||||
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cities_light.region'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalpark',
|
||||
name='city',
|
||||
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cities_light.city'),
|
||||
),
|
||||
]
|
||||
@@ -2,7 +2,7 @@ from django.db import models
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.utils.text import slugify
|
||||
from simple_history.models import HistoricalRecords
|
||||
import pycountry
|
||||
from cities_light.models import Country, Region, City
|
||||
|
||||
class Park(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
@@ -17,7 +17,9 @@ class Park(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(max_length=255, unique=True)
|
||||
location = models.CharField(max_length=255)
|
||||
country = models.CharField(max_length=2, help_text='Two-letter country code (ISO 3166-1 alpha-2)')
|
||||
country = models.ForeignKey(Country, on_delete=models.PROTECT)
|
||||
region = models.ForeignKey(Region, on_delete=models.PROTECT, null=True, blank=True)
|
||||
city = models.ForeignKey(City, on_delete=models.PROTECT, null=True, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
owner = models.ForeignKey(
|
||||
'companies.Company',
|
||||
@@ -62,6 +64,20 @@ class Park(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
|
||||
# Update the location field to combine country, region, and city
|
||||
location_parts = []
|
||||
if self.city:
|
||||
location_parts.append(self.city.name)
|
||||
if self.region:
|
||||
location_parts.append(self.region.name)
|
||||
if self.country:
|
||||
location_parts.append(self.country.name)
|
||||
|
||||
# Only update location if we have parts to combine
|
||||
if location_parts:
|
||||
self.location = ', '.join(location_parts)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -76,13 +92,16 @@ class Park(models.Model):
|
||||
return cls.objects.get(id=history.id), True
|
||||
raise cls.DoesNotExist("No park found with this slug")
|
||||
|
||||
def get_country_name(self):
|
||||
"""Get the full country name from the country code"""
|
||||
try:
|
||||
country = pycountry.countries.get(alpha_2=self.country)
|
||||
return country.name if country else self.country
|
||||
except:
|
||||
return self.country
|
||||
def get_formatted_location(self):
|
||||
"""Get a formatted location string combining city, region, and country"""
|
||||
location_parts = []
|
||||
if self.city:
|
||||
location_parts.append(self.city.name)
|
||||
if self.region:
|
||||
location_parts.append(self.region.name)
|
||||
if self.country:
|
||||
location_parts.append(self.country.name)
|
||||
return ', '.join(location_parts)
|
||||
|
||||
class ParkArea(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
@@ -8,8 +8,10 @@ urlpatterns = [
|
||||
path('', views.ParkListView.as_view(), name='park_list'),
|
||||
path('create/', views.ParkCreateView.as_view(), name='park_create'),
|
||||
path('rides/', RideListView.as_view(), name='all_rides'), # Global rides list
|
||||
path('countries/search/', views.search_countries, name='search_countries'),
|
||||
path('countries/select/', views.select_country, name='select_country'),
|
||||
path('ajax/countries/', views.get_countries, name='get_countries'),
|
||||
path('ajax/regions/', views.get_regions, name='get_regions'),
|
||||
path('ajax/cities/', views.get_cities, name='get_cities'),
|
||||
path('ajax/locations/', views.get_locations, name='get_locations'),
|
||||
path('<slug:slug>/', views.ParkDetailView.as_view(), name='park_detail'),
|
||||
path('<slug:park_slug>/rides/', include('rides.urls', namespace='rides')),
|
||||
]
|
||||
|
||||
122
parks/views.py
122
parks/views.py
@@ -7,17 +7,66 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import JsonResponse, HttpResponseRedirect, HttpResponse
|
||||
from .models import Park, ParkArea
|
||||
from .forms import ParkForm
|
||||
from rides.models import Ride
|
||||
from core.views import SlugRedirectMixin
|
||||
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, InlineEditMixin, HistoryMixin
|
||||
from moderation.models import EditSubmission
|
||||
import pycountry
|
||||
from cities_light.models import Country, Region, City
|
||||
|
||||
def get_countries(request):
|
||||
query = request.GET.get('q', '')
|
||||
countries = Country.objects.filter(name__icontains=query).values_list('name', flat=True)[:10]
|
||||
return JsonResponse(list(countries), safe=False)
|
||||
|
||||
def get_regions(request):
|
||||
query = request.GET.get('q', '')
|
||||
country = request.GET.get('country', '')
|
||||
if not country:
|
||||
return JsonResponse([], safe=False)
|
||||
|
||||
regions = Region.objects.filter(
|
||||
Q(name__icontains=query) | Q(alternate_names__icontains=query),
|
||||
country__name__iexact=country
|
||||
).values_list('name', flat=True)[:10]
|
||||
return JsonResponse(list(regions), safe=False)
|
||||
|
||||
def get_cities(request):
|
||||
query = request.GET.get('q', '')
|
||||
region = request.GET.get('region', '')
|
||||
country = request.GET.get('country', '')
|
||||
if not region or not country:
|
||||
return JsonResponse([], safe=False)
|
||||
|
||||
cities = City.objects.filter(
|
||||
Q(name__icontains=query) | Q(alternate_names__icontains=query),
|
||||
region__name__iexact=region,
|
||||
region__country__name__iexact=country
|
||||
).values_list('name', flat=True)[:10]
|
||||
return JsonResponse(list(cities), safe=False)
|
||||
|
||||
def get_locations(request):
|
||||
query = request.GET.get('q', '')
|
||||
locations = set()
|
||||
|
||||
# Search countries
|
||||
countries = Country.objects.filter(name__icontains=query).values_list('name', flat=True)[:5]
|
||||
locations.update(countries)
|
||||
|
||||
# Search regions
|
||||
regions = Region.objects.filter(name__icontains=query).values_list('name', flat=True)[:5]
|
||||
locations.update(regions)
|
||||
|
||||
# Search cities
|
||||
cities = City.objects.filter(name__icontains=query).values_list('name', flat=True)[:5]
|
||||
locations.update(cities)
|
||||
|
||||
return JsonResponse(list(locations), safe=False)
|
||||
|
||||
class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
model = Park
|
||||
form_class = ParkForm
|
||||
template_name = 'parks/park_form.html'
|
||||
fields = ['name', 'location', 'country', 'description', 'owner', 'status',
|
||||
'opening_date', 'closing_date', 'operating_season', 'size_acres', 'website']
|
||||
|
||||
def form_valid(self, form):
|
||||
# If user is moderator or above, save directly
|
||||
@@ -30,6 +79,12 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
# Convert model instances to IDs for JSON serialization
|
||||
if cleaned_data.get('owner'):
|
||||
cleaned_data['owner'] = cleaned_data['owner'].id
|
||||
if cleaned_data.get('country'):
|
||||
cleaned_data['country'] = cleaned_data['country'].id
|
||||
if cleaned_data.get('region'):
|
||||
cleaned_data['region'] = cleaned_data['region'].id
|
||||
if cleaned_data.get('city'):
|
||||
cleaned_data['city'] = cleaned_data['city'].id
|
||||
|
||||
submission = EditSubmission.objects.create(
|
||||
user=self.request.user,
|
||||
@@ -39,41 +94,10 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
reason=self.request.POST.get('reason', ''),
|
||||
source=self.request.POST.get('source', '')
|
||||
)
|
||||
return HttpResponseRedirect(reverse('park_list'))
|
||||
return HttpResponseRedirect(reverse('parks:park_list'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('park_detail', kwargs={'slug': self.object.slug})
|
||||
|
||||
def search_countries(request):
|
||||
query = request.GET.get('q', '').strip()
|
||||
countries = []
|
||||
|
||||
if query:
|
||||
# Use pycountry's search functionality for fuzzy matching
|
||||
try:
|
||||
# Try exact search first
|
||||
country = pycountry.countries.get(name=query)
|
||||
if country:
|
||||
countries = [country]
|
||||
else:
|
||||
# If no exact match, try fuzzy search
|
||||
countries = pycountry.countries.search_fuzzy(query)
|
||||
except LookupError:
|
||||
# If search fails, fallback to manual filtering
|
||||
countries = [
|
||||
country for country in pycountry.countries
|
||||
if query.lower() in country.name.lower()
|
||||
]
|
||||
|
||||
return render(request, 'parks/partials/country_search_results.html', {
|
||||
'countries': countries[:10] # Limit to top 10 results
|
||||
})
|
||||
|
||||
def select_country(request):
|
||||
if request.method == 'POST':
|
||||
country = request.POST.get('country', '')
|
||||
return HttpResponse(country)
|
||||
return HttpResponse('Invalid request', status=400)
|
||||
return reverse('parks:park_detail', kwargs={'slug': self.object.slug})
|
||||
|
||||
class ParkDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, InlineEditMixin, HistoryMixin, DetailView):
|
||||
model = Park
|
||||
@@ -96,7 +120,7 @@ class ParkDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixi
|
||||
return context
|
||||
|
||||
def get_redirect_url_pattern(self):
|
||||
return 'park_detail'
|
||||
return 'parks:park_detail'
|
||||
|
||||
class ParkAreaDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, InlineEditMixin, HistoryMixin, DetailView):
|
||||
model = ParkArea
|
||||
@@ -123,7 +147,7 @@ class ParkAreaDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmission
|
||||
return context
|
||||
|
||||
def get_redirect_url_pattern(self):
|
||||
return 'park_detail'
|
||||
return 'parks:park_detail'
|
||||
|
||||
def get_redirect_url_kwargs(self):
|
||||
return {
|
||||
@@ -137,7 +161,7 @@ class ParkListView(ListView):
|
||||
context_object_name = 'parks'
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Park.objects.select_related('owner').prefetch_related('photos', 'rides')
|
||||
queryset = Park.objects.select_related('owner', 'country', 'region', 'city').prefetch_related('photos', 'rides')
|
||||
|
||||
search = self.request.GET.get('search', '').strip() or None
|
||||
location = self.request.GET.get('location', '').strip() or None
|
||||
@@ -146,10 +170,19 @@ class ParkListView(ListView):
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search) |
|
||||
Q(location__icontains=search)
|
||||
Q(location__icontains=search) |
|
||||
Q(country__name__icontains=search) |
|
||||
Q(region__name__icontains=search) |
|
||||
Q(city__name__icontains=search)
|
||||
)
|
||||
if location:
|
||||
queryset = queryset.filter(location=location)
|
||||
# Try to match against the formatted location or any location field
|
||||
queryset = queryset.filter(
|
||||
Q(location__icontains=location) |
|
||||
Q(country__name__icontains=location) |
|
||||
Q(region__name__icontains=location) |
|
||||
Q(city__name__icontains=location)
|
||||
)
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
@@ -157,18 +190,11 @@ class ParkListView(ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Get unique locations for filter dropdown
|
||||
context['locations'] = list(Park.objects.values_list('location', flat=True)
|
||||
.distinct().order_by('location'))
|
||||
|
||||
# Add current filter values to context
|
||||
context['current_filters'] = {
|
||||
'search': self.request.GET.get('search', ''),
|
||||
'location': self.request.GET.get('location', ''),
|
||||
'status': self.request.GET.get('status', '')
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
}
|
||||
|
||||
.btn-discord {
|
||||
@apply bg-[#5865F2] hover:bg-[#4752C4] text-white border-transparent shadow-[#5865F2]/20;
|
||||
@apply text-gray-700 bg-white border-gray-200 hover:bg-gray-50 shadow-gray-200/50 dark:shadow-gray-900/50;
|
||||
}
|
||||
|
||||
.btn-google {
|
||||
@@ -1509,11 +1509,6 @@ select {
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
#mobileMenu.show {
|
||||
max-height: 300px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#mobileMenu .space-y-4 {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
@@ -1915,28 +1910,6 @@ select {
|
||||
color: rgb(209 213 219 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.form-hint > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.form-hint:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.form-error {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
@@ -2162,18 +2135,24 @@ select {
|
||||
}
|
||||
|
||||
.btn-discord {
|
||||
border-color: transparent;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(88 101 242 / var(--tw-bg-opacity));
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
--tw-shadow-color: rgb(88 101 242 / 0.2);
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
--tw-shadow-color: rgb(229 231 235 / 0.5);
|
||||
--tw-shadow: var(--tw-shadow-colored);
|
||||
}
|
||||
|
||||
.btn-discord:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(71 82 196 / var(--tw-bg-opacity));
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.btn-discord:is(.dark *) {
|
||||
--tw-shadow-color: rgb(17 24 39 / 0.5);
|
||||
--tw-shadow: var(--tw-shadow-colored);
|
||||
}
|
||||
|
||||
.btn-google {
|
||||
@@ -2211,23 +2190,6 @@ select {
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
border-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(187 247 208 / var(--tw-border-opacity));
|
||||
background-color: rgb(220 252 231 / 0.9);
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 101 52 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.alert-success:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(21 128 61 / var(--tw-border-opacity));
|
||||
background-color: rgb(22 101 52 / 0.3);
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 252 231 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
border-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
@@ -2245,40 +2207,6 @@ select {
|
||||
color: rgb(254 226 226 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
border-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(254 240 138 / var(--tw-border-opacity));
|
||||
background-color: rgb(254 249 195 / 0.9);
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.alert-warning:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(161 98 7 / var(--tw-border-opacity));
|
||||
background-color: rgb(133 77 14 / 0.3);
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 249 195 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
border-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(191 219 254 / var(--tw-border-opacity));
|
||||
background-color: rgb(219 234 254 / 0.9);
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.alert-info:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(29 78 216 / var(--tw-border-opacity));
|
||||
background-color: rgb(30 64 175 / 0.3);
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(219 234 254 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
/* Layout Components */
|
||||
|
||||
.card {
|
||||
@@ -2299,76 +2227,8 @@ select {
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
transition-property: transform;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
--tw-translate-y: -0.25rem;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.grid-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.grid-cards {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.grid-cards {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
|
||||
.heading-1 {
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
font-weight: 700;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.heading-1:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.heading-2 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 700;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.heading-2:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-body {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(75 85 99 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-body:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(209 213 219 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
/* Turnstile Widget */
|
||||
|
||||
.turnstile {
|
||||
@@ -2395,10 +2255,6 @@ select {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.top-1\/2 {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -2425,11 +2281,6 @@ select {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.my-6 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
@@ -2522,6 +2373,10 @@ select {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
@@ -2554,10 +2409,6 @@ select {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.max-h-\[300px\] {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.min-h-\[calc\(100vh-16rem\)\] {
|
||||
min-height: calc(100vh - 16rem);
|
||||
}
|
||||
@@ -2566,10 +2417,6 @@ select {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
@@ -2734,10 +2581,6 @@ select {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-2xl {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.rounded-full {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
@@ -2750,10 +2593,6 @@ select {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.rounded-l-lg {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
@@ -2776,11 +2615,6 @@ select {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-blue-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(191 219 254 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||
@@ -2795,21 +2629,11 @@ select {
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-green-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(187 247 208 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-green-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-red-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(254 202 202 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-red-400 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(248 113 113 / var(--tw-border-opacity));
|
||||
@@ -2820,29 +2644,11 @@ select {
|
||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-transparent {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.border-yellow-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(254 240 138 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-\[\#5865F2\] {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(88 101 242 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-100\/90 {
|
||||
background-color: rgb(219 234 254 / 0.9);
|
||||
}
|
||||
|
||||
.bg-blue-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 246 255 / var(--tw-bg-opacity));
|
||||
@@ -2873,28 +2679,16 @@ select {
|
||||
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-100\/90 {
|
||||
background-color: rgb(220 252 231 / 0.9);
|
||||
}
|
||||
|
||||
.bg-green-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary\/10 {
|
||||
background-color: rgb(79 70 229 / 0.1);
|
||||
}
|
||||
|
||||
.bg-red-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-red-100\/90 {
|
||||
background-color: rgb(254 226 226 / 0.9);
|
||||
}
|
||||
|
||||
.bg-red-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
@@ -2905,10 +2699,6 @@ select {
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-white\/70 {
|
||||
background-color: rgb(255 255 255 / 0.7);
|
||||
}
|
||||
|
||||
.bg-white\/90 {
|
||||
background-color: rgb(255 255 255 / 0.9);
|
||||
}
|
||||
@@ -2918,10 +2708,6 @@ select {
|
||||
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-yellow-100\/90 {
|
||||
background-color: rgb(254 249 195 / 0.9);
|
||||
}
|
||||
|
||||
.bg-gradient-to-br {
|
||||
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
|
||||
}
|
||||
@@ -2981,11 +2767,6 @@ select {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.px-1 {
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
@@ -3031,11 +2812,6 @@ select {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.py-2\.5 {
|
||||
padding-top: 0.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.py-3 {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
@@ -3079,11 +2855,6 @@ select {
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
@@ -3210,10 +2981,6 @@ select {
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
@@ -3238,17 +3005,6 @@ select {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-xl {
|
||||
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-gray-200\/50 {
|
||||
--tw-shadow-color: rgb(229 231 235 / 0.5);
|
||||
--tw-shadow: var(--tw-shadow-colored);
|
||||
}
|
||||
|
||||
.ring-2 {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
@@ -3269,12 +3025,6 @@ select {
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
||||
}
|
||||
|
||||
.backdrop-blur-sm {
|
||||
--tw-backdrop-blur: blur(4px);
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
||||
}
|
||||
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -3293,19 +3043,6 @@ select {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.duration-300 {
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.ease-in-out {
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.content-\[\'\'\] {
|
||||
--tw-content: '';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
|
||||
.dark\:prose-invert:is(.dark *) {
|
||||
--tw-prose-body: var(--tw-prose-invert-body);
|
||||
--tw-prose-headings: var(--tw-prose-invert-headings);
|
||||
@@ -3327,11 +3064,6 @@ select {
|
||||
--tw-prose-td-borders: var(--tw-prose-invert-td-borders);
|
||||
}
|
||||
|
||||
.first\:rounded-t-lg:first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.last\:mb-0:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
@@ -3352,21 +3084,6 @@ select {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.hover\:scale-\[1\.02\]:hover {
|
||||
--tw-scale-x: 1.02;
|
||||
--tw-scale-y: 1.02;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.hover\:border-primary\/20:hover {
|
||||
border-color: rgb(79 70 229 / 0.2);
|
||||
}
|
||||
|
||||
.hover\:bg-\[\#4752C4\]:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(71 82 196 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-blue-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
@@ -3377,11 +3094,6 @@ select {
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-200:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-300:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
||||
@@ -3397,25 +3109,11 @@ select {
|
||||
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-primary\/10:hover {
|
||||
background-color: rgb(79 70 229 / 0.1);
|
||||
}
|
||||
|
||||
.hover\:bg-red-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:from-primary\/90:hover {
|
||||
--tw-gradient-from: rgb(79 70 229 / 0.9) var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
|
||||
.hover\:to-secondary\/90:hover {
|
||||
--tw-gradient-to: rgb(225 29 72 / 0.9) var(--tw-gradient-to-position);
|
||||
}
|
||||
|
||||
.hover\:text-blue-500:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(59 130 246 / var(--tw-text-opacity));
|
||||
@@ -3505,10 +3203,6 @@ select {
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-800\/30:is(.dark *) {
|
||||
background-color: rgb(30 64 175 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-blue-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
|
||||
@@ -3537,10 +3231,6 @@ select {
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-800\/70:is(.dark *) {
|
||||
background-color: rgb(31 41 55 / 0.7);
|
||||
}
|
||||
|
||||
.dark\:bg-gray-800\/90:is(.dark *) {
|
||||
background-color: rgb(31 41 55 / 0.9);
|
||||
}
|
||||
@@ -3550,15 +3240,6 @@ select {
|
||||
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-green-700:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-green-800\/30:is(.dark *) {
|
||||
background-color: rgb(22 101 52 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-green-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(20 83 45 / var(--tw-bg-opacity));
|
||||
@@ -3569,15 +3250,6 @@ select {
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-700:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-800\/30:is(.dark *) {
|
||||
background-color: rgb(153 27 27 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-red-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
|
||||
@@ -3592,10 +3264,6 @@ select {
|
||||
background-color: rgb(202 138 4 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-800\/30:is(.dark *) {
|
||||
background-color: rgb(133 77 14 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
@@ -3616,11 +3284,6 @@ select {
|
||||
--tw-gradient-to: #3b0764 var(--tw-gradient-to-position);
|
||||
}
|
||||
|
||||
.dark\:text-blue-100:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(219 234 254 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-blue-200:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(191 219 254 / var(--tw-text-opacity));
|
||||
@@ -3651,26 +3314,11 @@ select {
|
||||
color: rgb(156 163 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-green-100:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 252 231 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-green-200:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(187 247 208 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-primary:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(79 70 229 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-100:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 226 226 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-200:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 202 202 / var(--tw-text-opacity));
|
||||
@@ -3686,11 +3334,6 @@ select {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-yellow-100:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 249 195 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-yellow-200:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(254 240 138 / var(--tw-text-opacity));
|
||||
@@ -3745,10 +3388,6 @@ select {
|
||||
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-primary\/20:hover:is(.dark *) {
|
||||
background-color: rgb(79 70 229 / 0.2);
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-red-600:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
|
||||
187
staticfiles/css/inline-edit.css
Normal file
187
staticfiles/css/inline-edit.css
Normal file
@@ -0,0 +1,187 @@
|
||||
/* Inline editing styles */
|
||||
.editable-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-editable] {
|
||||
position: relative;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
[data-editable]:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dark [data-editable]:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
[data-edit-button] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
transition: opacity 0.2s;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark [data-edit-button] {
|
||||
background-color: rgba(31, 41, 55, 0.9);
|
||||
}
|
||||
|
||||
.editable-container:hover [data-edit-button] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-input, .form-textarea, .form-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.375rem;
|
||||
background-color: white;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.dark .form-input, .dark .form-textarea, .dark .form-select {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-textarea:focus, .form-select:focus {
|
||||
outline: none;
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.dark .form-input:focus, .dark .form-textarea:focus, .dark .form-select:focus {
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
/* Notifications */
|
||||
.notification {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
max-width: 24rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
z-index: 50;
|
||||
animation: slide-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.dark .notification-success {
|
||||
background-color: #047857;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.dark .notification-error {
|
||||
background-color: #b91c1c;
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background-color: #3b82f6;
|
||||
}
|
||||
|
||||
.dark .notification-info {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add/Edit Form Styles */
|
||||
.form-section {
|
||||
@apply space-y-6;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
@apply mt-1 text-sm text-red-600 dark:text-red-400;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
@apply mt-1 text-sm text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 font-medium transition-colors rounded-lg;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply text-gray-700 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply text-white bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.status-operating {
|
||||
@apply text-green-800 bg-green-100 dark:bg-green-900 dark:text-green-200;
|
||||
}
|
||||
|
||||
.status-closed {
|
||||
@apply text-red-800 bg-red-100 dark:bg-red-900 dark:text-red-200;
|
||||
}
|
||||
|
||||
.status-construction {
|
||||
@apply text-yellow-800 bg-yellow-100 dark:bg-yellow-900 dark:text-yellow-200;
|
||||
}
|
||||
|
||||
/* Navigation Links */
|
||||
.nav-link {
|
||||
@apply flex items-center px-3 py-2 text-gray-700 transition-colors rounded-lg dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700;
|
||||
}
|
||||
|
||||
.nav-link i {
|
||||
@apply mr-2;
|
||||
}
|
||||
|
||||
/* Menu Items */
|
||||
.menu-item {
|
||||
@apply flex items-center w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700;
|
||||
}
|
||||
|
||||
.menu-item i {
|
||||
@apply mr-3;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
262
staticfiles/js/inline-edit.js
Normal file
262
staticfiles/js/inline-edit.js
Normal file
@@ -0,0 +1,262 @@
|
||||
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);
|
||||
}
|
||||
37
templates/environment_and_settings.html
Normal file
37
templates/environment_and_settings.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Django Environment and Settings</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Django Environment Variables</h1>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
{% for key, value in env_vars.items %}
|
||||
<tr>
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h1>Django Settings</h1>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>Setting</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
{% for key, value in settings_vars.items %}
|
||||
<tr>
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,48 +12,79 @@
|
||||
<form method="post" class="space-y-6">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Hidden fields -->
|
||||
{{ form.country }}
|
||||
{{ form.region }}
|
||||
{{ form.city }}
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Location fields -->
|
||||
<div>
|
||||
<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>
|
||||
<div>
|
||||
{{ form.country_name }}
|
||||
</div>
|
||||
{% if form.country_name.errors %}
|
||||
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{{ form.country_name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
<div>
|
||||
{{ form.region_name }}
|
||||
</div>
|
||||
{% if form.region_name.errors %}
|
||||
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{{ form.region_name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
<div>
|
||||
{{ form.city_name }}
|
||||
</div>
|
||||
{% if form.city_name.errors %}
|
||||
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{{ form.city_name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</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 }}
|
||||
</label>
|
||||
{% if field.name == 'country' %}
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
name="{{ field.name }}"
|
||||
id="country-input"
|
||||
value="{{ field.value|default:'' }}"
|
||||
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
hx-get="{% url 'parks:search_countries' %}"
|
||||
hx-trigger="keyup changed delay:200ms"
|
||||
hx-target="#country-results"
|
||||
hx-params="q"
|
||||
{% if field.field.required %}required{% endif %}>
|
||||
<div id="country-results" class="relative"></div>
|
||||
<div>
|
||||
{{ field }}
|
||||
</div>
|
||||
{% elif field.field.widget.input_type == 'text' or field.field.widget.input_type == 'date' %}
|
||||
<input type="{{ field.field.widget.input_type }}"
|
||||
name="{{ field.name }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
value="{{ field.value|default:'' }}"
|
||||
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
{% if field.field.required %}required{% endif %}>
|
||||
{% elif field.field.widget.input_type == 'select' %}
|
||||
<select name="{{ field.name }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
{% if field.field.required %}required{% endif %}>
|
||||
{% for value, label in field.field.choices %}
|
||||
<option value="{{ value }}" {% if field.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elif field.field.widget.input_type == 'textarea' %}
|
||||
<textarea name="{{ field.name }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
class="w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
rows="4"
|
||||
{% if field.field.required %}required{% endif %}>{{ field.value|default:'' }}</textarea>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>
|
||||
{% endif %}
|
||||
@@ -63,6 +94,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if not user.role == 'MODERATOR' and not user.role == 'ADMIN' and not user.role == 'SUPERUSER' %}
|
||||
@@ -104,3 +136,100 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css" />
|
||||
<style>
|
||||
.awesomplete {
|
||||
width: 100%;
|
||||
}
|
||||
.awesomplete > ul {
|
||||
@apply bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg;
|
||||
}
|
||||
.awesomplete > ul > li {
|
||||
@apply px-4 py-2 cursor-pointer text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
.awesomplete > ul > li:hover,
|
||||
.awesomplete > ul > li[aria-selected="true"] {
|
||||
@apply bg-gray-100 dark:bg-gray-600;
|
||||
}
|
||||
.awesomplete mark {
|
||||
@apply bg-blue-100 dark:bg-blue-900;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Helper function to initialize Awesomplete
|
||||
function initAwesomplete(input, url, params = {}) {
|
||||
if (!input) return null;
|
||||
|
||||
var awesomplete = new Awesomplete(input, {
|
||||
minChars: 1,
|
||||
maxItems: 10,
|
||||
autoFirst: true
|
||||
});
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
// Build query parameters
|
||||
const queryParams = new URLSearchParams({
|
||||
q: this.value,
|
||||
...Object.fromEntries(
|
||||
Object.entries(params).map(([key, value]) => [key, typeof value === 'function' ? value() : value])
|
||||
)
|
||||
});
|
||||
|
||||
fetch(`${url}?${queryParams}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
awesomplete.list = data;
|
||||
});
|
||||
});
|
||||
|
||||
return awesomplete;
|
||||
}
|
||||
|
||||
// Initialize Awesomplete for each location field
|
||||
var countryInput = document.getElementById('id_country_name');
|
||||
var regionInput = document.getElementById('id_region_name');
|
||||
var cityInput = document.getElementById('id_city_name');
|
||||
var countryHidden = document.getElementById('id_country');
|
||||
var regionHidden = document.getElementById('id_region');
|
||||
var cityHidden = document.getElementById('id_city');
|
||||
|
||||
var countryAwesomplete = initAwesomplete(countryInput, '/parks/ajax/countries/');
|
||||
|
||||
if (regionInput) {
|
||||
var regionAwesomplete = initAwesomplete(regionInput, '/parks/ajax/regions/', {
|
||||
country: () => countryInput ? countryInput.value : ''
|
||||
});
|
||||
|
||||
// Clear dependent fields when country changes
|
||||
countryInput.addEventListener('awesomplete-select', function(event) {
|
||||
regionInput.value = '';
|
||||
regionHidden.value = '';
|
||||
if (cityInput) {
|
||||
cityInput.value = '';
|
||||
cityHidden.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (cityInput) {
|
||||
var cityAwesomplete = initAwesomplete(cityInput, '/parks/ajax/cities/', {
|
||||
country: () => countryInput ? countryInput.value : '',
|
||||
region: () => regionInput ? regionInput.value : ''
|
||||
});
|
||||
|
||||
// Clear city when region changes
|
||||
regionInput.addEventListener('awesomplete-select', function(event) {
|
||||
cityInput.value = '';
|
||||
cityHidden.value = '';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="p-4 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<form class="grid grid-cols-1 gap-4 md:grid-cols-3"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-trigger="change from:select, input from:input[type='text']"
|
||||
hx-trigger="change from:select, input[type='text'] delay:500ms"
|
||||
hx-target="#parks-grid"
|
||||
hx-push-url="true">
|
||||
<div>
|
||||
@@ -30,15 +30,10 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="location" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Location</label>
|
||||
<select name="location" id="location"
|
||||
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">All Locations</option>
|
||||
{% for location in locations %}
|
||||
<option value="{{ location }}" {% if current_filters.location == location %}selected{% endif %}>
|
||||
{{ location }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" name="location" id="location"
|
||||
value="{{ current_filters.location }}"
|
||||
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Search locations...">
|
||||
</div>
|
||||
<div>
|
||||
<label for="status" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Status</label>
|
||||
@@ -62,3 +57,54 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css" />
|
||||
<style>
|
||||
.awesomplete {
|
||||
width: 100%;
|
||||
}
|
||||
.awesomplete > ul {
|
||||
@apply bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg;
|
||||
}
|
||||
.awesomplete > ul > li {
|
||||
@apply px-4 py-2 cursor-pointer text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
.awesomplete > ul > li:hover,
|
||||
.awesomplete > ul > li[aria-selected="true"] {
|
||||
@apply bg-gray-100 dark:bg-gray-600;
|
||||
}
|
||||
.awesomplete mark {
|
||||
@apply bg-blue-100 dark:bg-blue-900;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var locationInput = document.getElementById('location');
|
||||
if (locationInput) {
|
||||
var locationList = new Awesomplete(locationInput, {
|
||||
minChars: 1,
|
||||
maxItems: 10,
|
||||
autoFirst: true
|
||||
});
|
||||
|
||||
locationInput.addEventListener('input', function() {
|
||||
fetch(`/parks/ajax/locations/?q=${encodeURIComponent(this.value)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
locationList.list = data;
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger HTMX request when a location is selected
|
||||
locationInput.addEventListener('awesomplete-select', function(event) {
|
||||
htmx.trigger(this, 'change');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
4
templates/parks/partials/city_dropdown_list.html
Normal file
4
templates/parks/partials/city_dropdown_list.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<option value="">---------</option>
|
||||
{% for city in cities %}
|
||||
<option value="{{ city.pk }}">{{ city.name }}</option>
|
||||
{% endfor %}
|
||||
4
templates/parks/partials/region_dropdown_list.html
Normal file
4
templates/parks/partials/region_dropdown_list.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<option value="">---------</option>
|
||||
{% for region in regions %}
|
||||
<option value="{{ region.pk }}">{{ region.name }}</option>
|
||||
{% endfor %}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
||||
'django_htmx',
|
||||
'whitenoise',
|
||||
'django_tailwind_cli',
|
||||
'cities_light',
|
||||
|
||||
# Local apps
|
||||
'core',
|
||||
@@ -51,6 +52,11 @@ INSTALLED_APPS = [
|
||||
'moderation',
|
||||
]
|
||||
|
||||
# Cities Light settings
|
||||
CITIES_LIGHT_TRANSLATION_LANGUAGES = ['en']
|
||||
CITIES_LIGHT_INCLUDE_COUNTRIES = ['US', 'CA', 'GB', 'FR', 'DE', 'ES', 'IT', 'JP', 'CN', 'AU']
|
||||
CITIES_LIGHT_INCLUDE_CITY_TYPES = ['PPL', 'PPLA', 'PPLA2', 'PPLA3', 'PPLA4', 'PPLC', 'PPLG', 'PPLL', 'PPLR', 'PPLS']
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.cache.UpdateCacheMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
@@ -113,7 +119,7 @@ CACHES = {
|
||||
}
|
||||
}
|
||||
|
||||
CACHE_MIDDLEWARE_SECONDS = 300 # 5 minutes
|
||||
CACHE_MIDDLEWARE_SECONDS = 1 # 5 minutes
|
||||
CACHE_MIDDLEWARE_KEY_PREFIX = 'thrillwiki'
|
||||
|
||||
# Password validation
|
||||
@@ -139,10 +145,8 @@ USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = '/static/'
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'static'),
|
||||
]
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
||||
|
||||
# Media files
|
||||
@@ -209,9 +213,9 @@ FORWARD_EMAIL_BASE_URL = 'https://api.forwardemail.net'
|
||||
AUTH_USER_MODEL = 'accounts.User'
|
||||
|
||||
# Tailwind configuration
|
||||
TAILWIND_CLI_CONFIG_FILE = os.path.join(BASE_DIR, 'tailwind.config.js')
|
||||
TAILWIND_CLI_SRC_CSS = os.path.join(BASE_DIR, 'assets/css/src/input.css')
|
||||
TAILWIND_CLI_DIST_CSS = os.path.join(BASE_DIR, 'static/css/tailwind.css')
|
||||
TAILWIND_CLI_CONFIG_FILE = os.path.join(BASE_DIR, "tailwind.config.js")
|
||||
TAILWIND_CLI_SRC_CSS = os.path.join(BASE_DIR, "static/css/src/input.css")
|
||||
TAILWIND_CLI_DIST_CSS = os.path.join(BASE_DIR, "static/css/tailwind.css")
|
||||
|
||||
# Cloudflare Turnstile settings
|
||||
TURNSTILE_SITE_KEY = '0x4AAAAAAAyqVp3RjccrC9Kz'
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.conf.urls.static import static
|
||||
from accounts import views as accounts_views
|
||||
from django.views.generic import TemplateView
|
||||
from .views import HomeView, SearchView
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
@@ -40,6 +41,7 @@ urlpatterns = [
|
||||
|
||||
# Moderation URLs - placed after other URLs but before static/media serving
|
||||
path('moderation/', include('moderation.urls', namespace='moderation')),
|
||||
path('env-settings/', views***REMOVED***ironment_and_settings_view, name='environment_and_settings'),
|
||||
]
|
||||
|
||||
# Serve static files in development
|
||||
|
||||
@@ -4,6 +4,9 @@ from django.db.models import Count, Q
|
||||
from parks.models import Park
|
||||
from rides.models import Ride
|
||||
from companies.models import Company, Manufacturer
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
|
||||
def handler404(request, exception):
|
||||
return render(request, '404.html', status=404)
|
||||
@@ -73,3 +76,14 @@ class SearchView(TemplateView):
|
||||
).prefetch_related('rides')[:10]
|
||||
|
||||
return context
|
||||
def environment_and_settings_view(request):
|
||||
# Get all environment variables
|
||||
env_vars = dict(os***REMOVED***iron)
|
||||
|
||||
# Get all Django settings as a dictionary
|
||||
settings_vars = {setting: getattr(settings, setting) for setting in dir(settings) if setting.isupper()}
|
||||
|
||||
return render(request, 'environment_and_settings.html', {
|
||||
'env_vars': env_vars,
|
||||
'settings_vars': settings_vars
|
||||
})
|
||||
Reference in New Issue
Block a user