fixed the damn discord button

This commit is contained in:
pacnpal
2024-10-31 16:13:05 +00:00
parent 0075f7da6c
commit c1591af871
31 changed files with 1184 additions and 500 deletions

125
parks/forms.py Normal file
View 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

View File

@@ -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
),
),
]

View 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),
),
]

View 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'),
),
]

View File

@@ -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)

View File

@@ -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')),
]

View File

@@ -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):

View File

@@ -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 {

View File

@@ -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));

View 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

View 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);
}

View 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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -0,0 +1,4 @@
<option value="">---------</option>
{% for city in cities %}
<option value="{{ city.pk }}">{{ city.name }}</option>
{% endfor %}

View File

@@ -0,0 +1,4 @@
<option value="">---------</option>
{% for region in regions %}
<option value="{{ region.pk }}">{{ region.name }}</option>
{% endfor %}

View File

@@ -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'

View File

@@ -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

View File

@@ -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
})