mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:11:09 -05:00
weird header stuff
This commit is contained in:
@@ -7,14 +7,20 @@ class HistoryTrackingConfig(AppConfig):
|
||||
name = "history_tracking"
|
||||
|
||||
def ready(self):
|
||||
from django.apps import apps
|
||||
from .mixins import HistoricalChangeMixin
|
||||
from .models import Park
|
||||
|
||||
models_with_history = [Park]
|
||||
# Get the Park model
|
||||
try:
|
||||
Park = apps.get_model('parks', 'Park')
|
||||
ParkArea = apps.get_model('parks', 'ParkArea')
|
||||
|
||||
for model in models_with_history:
|
||||
# Check if mixin is already applied
|
||||
if HistoricalChangeMixin not in model.history.model.__bases__:
|
||||
model.history.model.__bases__ = (
|
||||
HistoricalChangeMixin,
|
||||
) + model.history.model.__bases__
|
||||
# Apply mixin to historical models
|
||||
if HistoricalChangeMixin not in Park.history.model.__bases__:
|
||||
Park.history.model.__bases__ = (HistoricalChangeMixin,) + Park.history.model.__bases__
|
||||
|
||||
if HistoricalChangeMixin not in ParkArea.history.model.__bases__:
|
||||
ParkArea.history.model.__bases__ = (HistoricalChangeMixin,) + ParkArea.history.model.__bases__
|
||||
except LookupError:
|
||||
# Models might not be loaded yet
|
||||
pass
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
|
||||
class HistoricalChangeMixin:
|
||||
@property
|
||||
def prev_record(self):
|
||||
"""Get the previous record for this instance"""
|
||||
try:
|
||||
return type(self).objects.filter(
|
||||
history_date__lt=self.history_date,
|
||||
id=self.id
|
||||
).order_by('-history_date').first()
|
||||
except (AttributeError, TypeError):
|
||||
return None
|
||||
|
||||
@property
|
||||
def diff_against_previous(self):
|
||||
prev_record = self.prev_record
|
||||
@@ -15,9 +26,14 @@ class HistoricalChangeMixin:
|
||||
"history_id",
|
||||
"history_type",
|
||||
"history_user_id",
|
||||
"history_change_reason",
|
||||
"history_type",
|
||||
] and not field.startswith("_"):
|
||||
try:
|
||||
old_value = getattr(prev_record, field)
|
||||
new_value = getattr(self, field)
|
||||
if old_value != new_value:
|
||||
changes[field] = {"old": old_value, "new": new_value}
|
||||
except AttributeError:
|
||||
continue
|
||||
return changes
|
||||
|
||||
@@ -4,14 +4,11 @@ from simple_history.models import HistoricalRecords
|
||||
from .mixins import HistoricalChangeMixin
|
||||
|
||||
|
||||
class Park(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
# ... other fields ...
|
||||
history = HistoricalRecords()
|
||||
class HistoricalModel(models.Model):
|
||||
"""Abstract base class for models with history tracking"""
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def _history_model(self):
|
||||
return self.history.model
|
||||
|
||||
|
||||
# Apply the mixin
|
||||
|
||||
@@ -202,8 +202,8 @@ class HistoryMixin:
|
||||
context = super().get_context_data(**kwargs)
|
||||
obj = self.get_object()
|
||||
|
||||
# Get historical records
|
||||
context['history'] = obj.history.all().select_related('history_user')
|
||||
# Get historical records ordered by date
|
||||
context['history'] = obj.history.all().select_related('history_user').order_by('-history_date')
|
||||
|
||||
# Get related edit submissions
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -33,27 +33,3 @@ class ParkAreaAdmin(SimpleHistoryAdmin):
|
||||
# Register the models with their admin classes
|
||||
admin.site.register(Park, ParkAdmin)
|
||||
admin.site.register(ParkArea, ParkAreaAdmin)
|
||||
|
||||
# Register the historical records for direct editing
|
||||
from simple_history.models import HistoricalRecords
|
||||
from .models import Park
|
||||
|
||||
class HistoricalParkAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'formatted_location', 'status', 'history_date', 'history_user', 'history_type')
|
||||
list_filter = ('status', 'history_type')
|
||||
search_fields = ('name', 'description')
|
||||
readonly_fields = ('history_date', 'history_type', 'history_id')
|
||||
|
||||
def formatted_location(self, obj):
|
||||
"""Display formatted location string"""
|
||||
return obj.instance.formatted_location
|
||||
formatted_location.short_description = 'Location'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False # Prevent adding new historical records directly
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False # Prevent deleting historical records
|
||||
|
||||
# Register the historical model
|
||||
admin.site.register(Park.history.model, HistoricalParkAdmin)
|
||||
|
||||
@@ -5,4 +5,4 @@ class ParksConfig(AppConfig):
|
||||
name = 'parks'
|
||||
|
||||
def ready(self):
|
||||
import parks.signals # noqa
|
||||
import parks.signals # Register signals
|
||||
|
||||
34
parks/management/commands/update_park_counts.py
Normal file
34
parks/management/commands/update_park_counts.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Count, Q
|
||||
from parks.models import Park
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Update total_rides and total_roller_coasters counts for all parks'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
parks = Park.objects.all()
|
||||
operating_rides = Q(status='OPERATING')
|
||||
updated = 0
|
||||
|
||||
for park in parks:
|
||||
# Count total operating rides
|
||||
total_rides = park.rides.filter(operating_rides).count()
|
||||
|
||||
# Count total operating roller coasters
|
||||
total_coasters = park.rides.filter(
|
||||
operating_rides,
|
||||
category='RC'
|
||||
).count()
|
||||
|
||||
# Update park counts
|
||||
Park.objects.filter(id=park.id).update(
|
||||
total_rides=total_rides,
|
||||
total_roller_coasters=total_coasters
|
||||
)
|
||||
updated += 1
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'Successfully updated counts for {updated} parks'
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-03 20:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0006_alter_historicalpark_latitude_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="historicalparkarea",
|
||||
name="history_user",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalparkarea",
|
||||
name="park",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="HistoricalPark",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="HistoricalParkArea",
|
||||
),
|
||||
]
|
||||
212
parks/migrations/0008_historicalpark_historicalparkarea.py
Normal file
212
parks/migrations/0008_historicalpark_historicalparkarea.py
Normal file
@@ -0,0 +1,212 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-03 20:38
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import history_tracking.mixins
|
||||
import parks.models
|
||||
import simple_history.models
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("companies", "0004_add_total_parks"),
|
||||
("parks", "0007_remove_historicalparkarea_history_user_and_more"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="HistoricalPark",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigIntegerField(
|
||||
auto_created=True, blank=True, db_index=True, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("slug", models.SlugField(max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("OPERATING", "Operating"),
|
||||
("CLOSED_TEMP", "Temporarily Closed"),
|
||||
("CLOSED_PERM", "Permanently Closed"),
|
||||
("UNDER_CONSTRUCTION", "Under Construction"),
|
||||
("DEMOLISHED", "Demolished"),
|
||||
("RELOCATED", "Relocated"),
|
||||
],
|
||||
default="OPERATING",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"latitude",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
help_text="Latitude coordinate (-90 to 90)",
|
||||
max_digits=9,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(Decimal("-90")),
|
||||
django.core.validators.MaxValueValidator(Decimal("90")),
|
||||
parks.models.validate_latitude_digits,
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
"longitude",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
help_text="Longitude coordinate (-180 to 180)",
|
||||
max_digits=10,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(Decimal("-180")),
|
||||
django.core.validators.MaxValueValidator(Decimal("180")),
|
||||
parks.models.validate_longitude_digits,
|
||||
],
|
||||
),
|
||||
),
|
||||
("street_address", models.CharField(blank=True, max_length=255)),
|
||||
("city", models.CharField(blank=True, max_length=255)),
|
||||
("state", models.CharField(blank=True, max_length=255)),
|
||||
("country", models.CharField(blank=True, max_length=255)),
|
||||
("postal_code", models.CharField(blank=True, max_length=20)),
|
||||
("opening_date", models.DateField(blank=True, null=True)),
|
||||
("closing_date", models.DateField(blank=True, null=True)),
|
||||
("operating_season", models.CharField(blank=True, max_length=255)),
|
||||
(
|
||||
"size_acres",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=10, null=True
|
||||
),
|
||||
),
|
||||
("website", models.URLField(blank=True)),
|
||||
(
|
||||
"average_rating",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=3, null=True
|
||||
),
|
||||
),
|
||||
("total_rides", models.IntegerField(blank=True, null=True)),
|
||||
("total_roller_coasters", models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(blank=True, editable=False, null=True),
|
||||
),
|
||||
("updated_at", models.DateTimeField(blank=True, editable=False)),
|
||||
("history_id", models.AutoField(primary_key=True, serialize=False)),
|
||||
("history_date", models.DateTimeField(db_index=True)),
|
||||
("history_change_reason", models.CharField(max_length=100, null=True)),
|
||||
(
|
||||
"history_type",
|
||||
models.CharField(
|
||||
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
|
||||
max_length=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"history_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="companies.company",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "historical park",
|
||||
"verbose_name_plural": "historical parks",
|
||||
"ordering": ("-history_date", "-history_id"),
|
||||
"get_latest_by": ("history_date", "history_id"),
|
||||
},
|
||||
bases=(
|
||||
history_tracking.mixins.HistoricalChangeMixin,
|
||||
simple_history.models.HistoricalChanges,
|
||||
models.Model,
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="HistoricalParkArea",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigIntegerField(
|
||||
auto_created=True, blank=True, db_index=True, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("slug", models.SlugField(max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("opening_date", models.DateField(blank=True, null=True)),
|
||||
("closing_date", models.DateField(blank=True, null=True)),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(blank=True, editable=False, null=True),
|
||||
),
|
||||
("updated_at", models.DateTimeField(blank=True, editable=False)),
|
||||
("history_id", models.AutoField(primary_key=True, serialize=False)),
|
||||
("history_date", models.DateTimeField(db_index=True)),
|
||||
("history_change_reason", models.CharField(max_length=100, null=True)),
|
||||
(
|
||||
"history_type",
|
||||
models.CharField(
|
||||
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
|
||||
max_length=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"history_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"park",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "historical park area",
|
||||
"verbose_name_plural": "historical park areas",
|
||||
"ordering": ("-history_date", "-history_id"),
|
||||
"get_latest_by": ("history_date", "history_id"),
|
||||
},
|
||||
bases=(
|
||||
history_tracking.mixins.HistoricalChangeMixin,
|
||||
simple_history.models.HistoricalChanges,
|
||||
models.Model,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -9,6 +9,7 @@ from simple_history.models import HistoricalRecords
|
||||
|
||||
from companies.models import Company
|
||||
from media.models import Photo
|
||||
from history_tracking.models import HistoricalModel
|
||||
|
||||
|
||||
def normalize_coordinate(value, max_digits, decimal_places):
|
||||
@@ -53,7 +54,7 @@ def validate_longitude_digits(value):
|
||||
return validate_coordinate_digits(value, 10, 6)
|
||||
|
||||
|
||||
class Park(models.Model):
|
||||
class Park(HistoricalModel):
|
||||
STATUS_CHOICES = [
|
||||
("OPERATING", "Operating"),
|
||||
("CLOSED_TEMP", "Temporarily Closed"),
|
||||
@@ -176,7 +177,7 @@ class Park(models.Model):
|
||||
raise cls.DoesNotExist()
|
||||
|
||||
|
||||
class ParkArea(models.Model):
|
||||
class ParkArea(HistoricalModel):
|
||||
park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name="areas")
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(max_length=255)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.db.models import Count, Q
|
||||
|
||||
from rides.models import Ride
|
||||
from .models import Park
|
||||
|
||||
def update_park_ride_counts(park):
|
||||
"""Update total_rides and total_roller_coasters for a park"""
|
||||
operating_rides = Q(status='OPERATING')
|
||||
|
||||
# Count total operating rides
|
||||
total_rides = park.rides.filter(operating_rides).count()
|
||||
|
||||
# Count total operating roller coasters
|
||||
total_coasters = park.rides.filter(
|
||||
operating_rides,
|
||||
category='RC'
|
||||
).count()
|
||||
|
||||
# Update park counts
|
||||
Park.objects.filter(id=park.id).update(
|
||||
total_rides=total_rides,
|
||||
total_roller_coasters=total_coasters
|
||||
)
|
||||
|
||||
@receiver(post_save, sender=Ride)
|
||||
def ride_saved(sender, instance, **kwargs):
|
||||
"""Update park counts when a ride is saved"""
|
||||
update_park_ride_counts(instance.park)
|
||||
|
||||
@receiver(post_delete, sender=Ride)
|
||||
def ride_deleted(sender, instance, **kwargs):
|
||||
"""Update park counts when a ride is deleted"""
|
||||
update_park_ride_counts(instance.park)
|
||||
|
||||
@@ -2237,6 +2237,22 @@ select {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.col-span-4 {
|
||||
grid-column: span 4 / span 4;
|
||||
}
|
||||
|
||||
.col-span-8 {
|
||||
grid-column: span 8 / span 8;
|
||||
}
|
||||
|
||||
.col-span-9 {
|
||||
grid-column: span 9 / span 9;
|
||||
}
|
||||
|
||||
.col-span-1 {
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
|
||||
.mx-1 {
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
@@ -2337,6 +2353,14 @@ select {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.mt-0\.5 {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@@ -2405,6 +2429,10 @@ select {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-\[340px\] {
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
.max-h-60 {
|
||||
max-height: 15rem;
|
||||
}
|
||||
@@ -2421,6 +2449,10 @@ select {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.min-h-0 {
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
.w-16 {
|
||||
width: 4rem;
|
||||
}
|
||||
@@ -2547,6 +2579,14 @@ select {
|
||||
resize: both;
|
||||
}
|
||||
|
||||
.auto-rows-fr {
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.auto-rows-min {
|
||||
grid-auto-rows: min-content;
|
||||
}
|
||||
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
@@ -2559,6 +2599,14 @@ select {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-12 {
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -2603,6 +2651,37 @@ select {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.gap-8 {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.gap-x-8 {
|
||||
-moz-column-gap: 2rem;
|
||||
column-gap: 2rem;
|
||||
}
|
||||
|
||||
.gap-y-6 {
|
||||
row-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.gap-x-6 {
|
||||
-moz-column-gap: 1.5rem;
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.gap-y-4 {
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.gap-x-4 {
|
||||
-moz-column-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
@@ -3156,6 +3235,11 @@ select {
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 163 74 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -3316,6 +3400,12 @@ 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-gray-300:hover {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||
@@ -3670,6 +3760,11 @@ select {
|
||||
color: rgb(254 252 232 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-green-400:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(74 222 128 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:ring-1:is(.dark *) {
|
||||
--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(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
@@ -3744,10 +3839,22 @@ select {
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.sm\:col-span-4 {
|
||||
grid-column: span 4 / span 4;
|
||||
}
|
||||
|
||||
.sm\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sm\:grid-cols-6 {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||
@@ -3762,6 +3869,26 @@ select {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.md\:col-span-3 {
|
||||
grid-column: span 3 / span 3;
|
||||
}
|
||||
|
||||
.md\:col-span-9 {
|
||||
grid-column: span 9 / span 9;
|
||||
}
|
||||
|
||||
.md\:col-span-4 {
|
||||
grid-column: span 4 / span 4;
|
||||
}
|
||||
|
||||
.md\:col-span-8 {
|
||||
grid-column: span 8 / span 8;
|
||||
}
|
||||
|
||||
.md\:mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
@@ -3778,6 +3905,14 @@ select {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.md\:grid-cols-8 {
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.md\:grid-cols-12 {
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.md\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -3786,6 +3921,10 @@ select {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.md\:justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.md\:text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
@@ -3806,6 +3945,22 @@ select {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.lg\:col-span-4 {
|
||||
grid-column: span 4 / span 4;
|
||||
}
|
||||
|
||||
.lg\:col-span-8 {
|
||||
grid-column: span 8 / span 8;
|
||||
}
|
||||
|
||||
.lg\:col-span-3 {
|
||||
grid-column: span 3 / span 3;
|
||||
}
|
||||
|
||||
.lg\:col-span-5 {
|
||||
grid-column: span 5 / span 5;
|
||||
}
|
||||
|
||||
.lg\:mb-0 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
@@ -3814,6 +3969,14 @@ select {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
|
||||
.lg\:ml-8 {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.lg\:mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.lg\:flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -3826,6 +3989,10 @@ select {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.lg\:w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.lg\:flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
@@ -3838,6 +4005,18 @@ select {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:grid-cols-12 {
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:grid-cols-5 {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:grid-cols-6 {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -3855,3 +4034,9 @@ select {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.xl\:grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
29
static/js/park-map.js
Normal file
29
static/js/park-map.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Only declare parkMap if it doesn't exist
|
||||
window.parkMap = window.parkMap || null;
|
||||
|
||||
function initParkMap(latitude, longitude, name) {
|
||||
const mapContainer = document.getElementById('park-map');
|
||||
|
||||
// Only initialize if container exists and map hasn't been initialized
|
||||
if (mapContainer && !window.parkMap) {
|
||||
const width = mapContainer.offsetWidth;
|
||||
mapContainer.style.height = width + 'px';
|
||||
|
||||
window.parkMap = L.map('park-map').setView([latitude, longitude], 13);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(window.parkMap);
|
||||
|
||||
L.marker([latitude, longitude])
|
||||
.addTo(window.parkMap)
|
||||
.bindPopup(name);
|
||||
|
||||
// Update map size when window is resized
|
||||
window.addEventListener('resize', function() {
|
||||
const width = mapContainer.offsetWidth;
|
||||
mapContainer.style.height = width + 'px';
|
||||
window.parkMap.invalidateSize();
|
||||
});
|
||||
}
|
||||
}
|
||||
44
staticfiles/css/alerts.css
Normal file
44
staticfiles/css/alerts.css
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Alert Styles */
|
||||
.alert {
|
||||
@apply fixed z-50 px-4 py-3 transition-all duration-500 transform rounded-lg shadow-lg right-4 top-4;
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
@apply text-white bg-green-500;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
@apply text-white bg-red-500;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
@apply text-white bg-blue-500;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
@apply text-white bg-yellow-500;
|
||||
}
|
||||
|
||||
/* Animation keyframes */
|
||||
@keyframes slideIn {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
282
staticfiles/css/src/input.css
Normal file
282
staticfiles/css/src/input.css
Normal file
@@ -0,0 +1,282 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
/* Button Styles */
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center px-6 py-2.5 border border-transparent rounded-full shadow-md text-sm font-medium text-white bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary/50 transform hover:scale-105 transition-all;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply inline-flex items-center px-6 py-2.5 border border-gray-200 dark:border-gray-700 rounded-full shadow-md text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary/50 transform hover:scale-105 transition-all;
|
||||
}
|
||||
/* [Previous styles remain unchanged until mobile menu section...] */
|
||||
|
||||
/* Mobile Menu */
|
||||
#mobileMenu {
|
||||
@apply overflow-hidden transition-all duration-300 ease-in-out opacity-0 max-h-0;
|
||||
}
|
||||
|
||||
#mobileMenu.show {
|
||||
@apply max-h-[300px] opacity-100;
|
||||
}
|
||||
|
||||
#mobileMenu .space-y-4 {
|
||||
@apply pb-6;
|
||||
}
|
||||
|
||||
.mobile-nav-link {
|
||||
@apply flex items-center justify-center px-6 py-3 text-gray-700 transition-all border border-transparent rounded-lg dark:text-gray-200 hover:bg-primary/10 dark:hover:bg-primary/20 hover:text-primary dark:hover:text-primary hover:border-primary/20 dark:hover:border-primary/30;
|
||||
}
|
||||
|
||||
.mobile-nav-link i {
|
||||
@apply text-xl transition-colors;
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.mobile-nav-link i {
|
||||
@apply text-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-nav-link.primary {
|
||||
@apply text-white bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90;
|
||||
}
|
||||
|
||||
.mobile-nav-link.primary i {
|
||||
@apply mr-3 text-white;
|
||||
}
|
||||
|
||||
.mobile-nav-link.secondary {
|
||||
@apply text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600;
|
||||
}
|
||||
|
||||
.mobile-nav-link.secondary i {
|
||||
@apply mr-3 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
/* Theme Toggle */
|
||||
#theme-toggle+.theme-toggle-btn i::before {
|
||||
content: "\f186";
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
#theme-toggle:checked+.theme-toggle-btn i::before {
|
||||
content: "\f185";
|
||||
color: theme('colors.yellow.400');
|
||||
}
|
||||
|
||||
/* Navigation Components */
|
||||
.nav-link {
|
||||
@apply flex items-center text-gray-700 transition-all dark:text-gray-200;
|
||||
}
|
||||
|
||||
/* Extra small screens (540px and below) */
|
||||
@media (max-width: 540px) {
|
||||
.nav-link {
|
||||
@apply px-2 py-2 text-sm;
|
||||
}
|
||||
.nav-link i {
|
||||
@apply mr-1 text-base;
|
||||
}
|
||||
.nav-link span {
|
||||
@apply text-sm;
|
||||
}
|
||||
.site-logo {
|
||||
@apply px-1 text-lg;
|
||||
}
|
||||
.nav-container {
|
||||
@apply px-2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small screens (541px to 767px) */
|
||||
@media (min-width: 541px) and (max-width: 767px) {
|
||||
.nav-link {
|
||||
@apply px-3 py-2;
|
||||
}
|
||||
.nav-link i {
|
||||
@apply mr-2;
|
||||
}
|
||||
.site-logo {
|
||||
@apply px-2 text-xl;
|
||||
}
|
||||
.nav-container {
|
||||
@apply px-4;
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium screens and up */
|
||||
@media (min-width: 768px) {
|
||||
.nav-link {
|
||||
@apply px-6 py-2.5 rounded-lg font-medium border border-transparent hover:border-primary/20 dark:hover:border-primary/30;
|
||||
}
|
||||
.nav-link i {
|
||||
@apply mr-3 text-lg;
|
||||
}
|
||||
.site-logo {
|
||||
@apply px-3 text-2xl;
|
||||
}
|
||||
.nav-container {
|
||||
@apply px-6;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
@apply text-primary dark:text-primary bg-primary/10 dark:bg-primary/20;
|
||||
}
|
||||
|
||||
.nav-link i {
|
||||
@apply text-gray-500 transition-colors dark:text-gray-400;
|
||||
}
|
||||
|
||||
.nav-link:hover i {
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
#mobileMenu {
|
||||
@apply hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Menu Items */
|
||||
.menu-item {
|
||||
@apply flex items-center w-full px-4 py-3 text-sm text-gray-700 transition-all dark:text-gray-200 hover:bg-primary/10 dark:hover:bg-primary/20 hover:text-primary dark:hover:text-primary first:rounded-t-lg last:rounded-b-lg;
|
||||
}
|
||||
|
||||
.menu-item i {
|
||||
@apply mr-3 text-base text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
/* Form Components */
|
||||
.form-input {
|
||||
@apply w-full px-4 py-3 text-gray-900 transition-all border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm dark:text-white focus:ring-2 focus:ring-primary/50 focus:border-primary;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
@apply mt-2 space-y-1 text-sm text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
@apply mt-2 text-sm text-red-600 dark:text-red-400;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
@apply inline-flex items-center px-3 py-1 text-sm font-medium rounded-full;
|
||||
}
|
||||
|
||||
.status-operating {
|
||||
@apply text-green-800 bg-green-100 dark:bg-green-700 dark:text-green-50;
|
||||
}
|
||||
|
||||
.status-closed {
|
||||
@apply text-red-800 bg-red-100 dark:bg-red-700 dark:text-red-50;
|
||||
}
|
||||
|
||||
.status-construction {
|
||||
@apply text-yellow-800 bg-yellow-100 dark:bg-yellow-600 dark:text-yellow-50;
|
||||
}
|
||||
|
||||
/* Auth Components */
|
||||
.auth-card {
|
||||
@apply w-full max-w-md p-8 mx-auto border shadow-xl bg-white/90 dark:bg-gray-800/90 rounded-2xl backdrop-blur-sm border-gray-200/50 dark:border-gray-700/50;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
@apply mb-8 text-2xl font-bold text-center text-transparent bg-gradient-to-r from-primary to-secondary bg-clip-text;
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
@apply relative my-6 text-center;
|
||||
}
|
||||
|
||||
.auth-divider::before,
|
||||
.auth-divider::after {
|
||||
@apply absolute top-1/2 w-1/3 border-t border-gray-200 dark:border-gray-700 content-[''];
|
||||
}
|
||||
|
||||
.auth-divider::before {
|
||||
@apply left-0;
|
||||
}
|
||||
|
||||
.auth-divider::after {
|
||||
@apply right-0;
|
||||
}
|
||||
|
||||
.auth-divider span {
|
||||
@apply px-4 text-sm text-gray-500 dark:text-gray-400 bg-white/90 dark:bg-gray-800/90;
|
||||
}
|
||||
|
||||
/* Social Login Buttons */
|
||||
.btn-social {
|
||||
@apply w-full flex items-center justify-center px-6 py-3 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary/50 transform hover:scale-[1.02] transition-all mb-3;
|
||||
}
|
||||
|
||||
.btn-discord {
|
||||
@apply text-gray-700 bg-white border-gray-200 hover:bg-gray-50 shadow-gray-200/50 dark:shadow-gray-900/50;
|
||||
}
|
||||
|
||||
.btn-google {
|
||||
@apply text-gray-700 bg-white border-gray-200 hover:bg-gray-50 shadow-gray-200/50 dark:shadow-gray-900/50;
|
||||
}
|
||||
|
||||
/* Alert Components */
|
||||
.alert {
|
||||
@apply p-4 mb-4 shadow-lg rounded-xl backdrop-blur-sm;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
@apply text-green-800 border border-green-200 bg-green-100/90 dark:bg-green-800/30 dark:text-green-100 dark:border-green-700;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
@apply text-red-800 border border-red-200 bg-red-100/90 dark:bg-red-800/30 dark:text-red-100 dark:border-red-700;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
@apply text-yellow-800 border border-yellow-200 bg-yellow-100/90 dark:bg-yellow-800/30 dark:text-yellow-100 dark:border-yellow-700;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
@apply text-blue-800 border border-blue-200 bg-blue-100/90 dark:bg-blue-800/30 dark:text-blue-100 dark:border-blue-700;
|
||||
}
|
||||
|
||||
/* Layout Components */
|
||||
.card {
|
||||
@apply p-6 bg-white border rounded-lg shadow-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
@apply transition-transform transform hover:-translate-y-1;
|
||||
}
|
||||
|
||||
.grid-cards {
|
||||
@apply grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.heading-1 {
|
||||
@apply mb-6 text-3xl font-bold text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
.heading-2 {
|
||||
@apply mb-4 text-2xl font-bold text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
.text-body {
|
||||
@apply text-gray-600 dark:text-gray-300;
|
||||
}
|
||||
|
||||
/* Turnstile Widget */
|
||||
.turnstile {
|
||||
@apply flex items-center justify-center my-4;
|
||||
}
|
||||
}
|
||||
3867
staticfiles/css/tailwind.css
Normal file
3867
staticfiles/css/tailwind.css
Normal file
File diff suppressed because it is too large
Load Diff
0
staticfiles/images/placeholders/dark-ride.jpg
Normal file
0
staticfiles/images/placeholders/dark-ride.jpg
Normal file
0
staticfiles/images/placeholders/default-park.jpg
Normal file
0
staticfiles/images/placeholders/default-park.jpg
Normal file
0
staticfiles/images/placeholders/default-ride.jpg
Normal file
0
staticfiles/images/placeholders/default-ride.jpg
Normal file
0
staticfiles/images/placeholders/flat-ride.jpg
Normal file
0
staticfiles/images/placeholders/flat-ride.jpg
Normal file
0
staticfiles/images/placeholders/other-ride.jpg
Normal file
0
staticfiles/images/placeholders/other-ride.jpg
Normal file
0
staticfiles/images/placeholders/roller-coaster.jpg
Normal file
0
staticfiles/images/placeholders/roller-coaster.jpg
Normal file
0
staticfiles/images/placeholders/transport.jpg
Normal file
0
staticfiles/images/placeholders/transport.jpg
Normal file
0
staticfiles/images/placeholders/water-ride.jpg
Normal file
0
staticfiles/images/placeholders/water-ride.jpg
Normal file
18
staticfiles/js/alerts.js
Normal file
18
staticfiles/js/alerts.js
Normal file
@@ -0,0 +1,18 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get all alert elements
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
|
||||
// For each alert
|
||||
alerts.forEach(alert => {
|
||||
// After 5 seconds
|
||||
setTimeout(() => {
|
||||
// Add slideOut animation
|
||||
alert.style.animation = 'slideOut 0.5s ease-out forwards';
|
||||
|
||||
// Remove the alert after animation completes
|
||||
setTimeout(() => {
|
||||
alert.remove();
|
||||
}, 500);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
5
staticfiles/js/alpine.min.js
vendored
Normal file
5
staticfiles/js/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
81
staticfiles/js/location-autocomplete.js
Normal file
81
staticfiles/js/location-autocomplete.js
Normal file
@@ -0,0 +1,81 @@
|
||||
function locationAutocomplete(field, filterParks = false) {
|
||||
return {
|
||||
query: '',
|
||||
suggestions: [],
|
||||
fetchSuggestions() {
|
||||
let url;
|
||||
const params = new URLSearchParams({
|
||||
q: this.query,
|
||||
filter_parks: filterParks
|
||||
});
|
||||
|
||||
switch (field) {
|
||||
case 'country':
|
||||
url = '/parks/ajax/countries/';
|
||||
break;
|
||||
case 'region':
|
||||
url = '/parks/ajax/regions/';
|
||||
// Add country parameter if we're fetching regions
|
||||
const countryInput = document.getElementById(filterParks ? 'country' : 'id_country_name');
|
||||
if (countryInput && countryInput.value) {
|
||||
params.append('country', countryInput.value);
|
||||
}
|
||||
break;
|
||||
case 'city':
|
||||
url = '/parks/ajax/cities/';
|
||||
// Add country and region parameters if we're fetching cities
|
||||
const regionInput = document.getElementById(filterParks ? 'region' : 'id_region_name');
|
||||
const cityCountryInput = document.getElementById(filterParks ? 'country' : 'id_country_name');
|
||||
if (regionInput && regionInput.value && cityCountryInput && cityCountryInput.value) {
|
||||
params.append('country', cityCountryInput.value);
|
||||
params.append('region', regionInput.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
fetch(`${url}?${params}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.suggestions = data;
|
||||
});
|
||||
}
|
||||
},
|
||||
selectSuggestion(suggestion) {
|
||||
this.query = suggestion.name;
|
||||
this.suggestions = [];
|
||||
|
||||
// If this is a form field (not filter), update hidden fields
|
||||
if (!filterParks) {
|
||||
const hiddenField = document.getElementById(`id_${field}`);
|
||||
if (hiddenField) {
|
||||
hiddenField.value = suggestion.id;
|
||||
}
|
||||
|
||||
// Clear dependent fields when parent field changes
|
||||
if (field === 'country') {
|
||||
const regionInput = document.getElementById('id_region_name');
|
||||
const cityInput = document.getElementById('id_city_name');
|
||||
const regionHidden = document.getElementById('id_region');
|
||||
const cityHidden = document.getElementById('id_city');
|
||||
|
||||
if (regionInput) regionInput.value = '';
|
||||
if (cityInput) cityInput.value = '';
|
||||
if (regionHidden) regionHidden.value = '';
|
||||
if (cityHidden) cityHidden.value = '';
|
||||
} else if (field === 'region') {
|
||||
const cityInput = document.getElementById('id_city_name');
|
||||
const cityHidden = document.getElementById('id_city');
|
||||
|
||||
if (cityInput) cityInput.value = '';
|
||||
if (cityHidden) cityHidden.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger form submission for filters
|
||||
if (filterParks) {
|
||||
htmx.trigger('#park-filters', 'change');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
28
staticfiles/js/park-map.js
Normal file
28
staticfiles/js/park-map.js
Normal file
@@ -0,0 +1,28 @@
|
||||
let parkMap = null;
|
||||
|
||||
function initParkMap(latitude, longitude, name) {
|
||||
const mapContainer = document.getElementById('park-map');
|
||||
|
||||
// Only initialize if container exists and map hasn't been initialized
|
||||
if (mapContainer && !parkMap) {
|
||||
const width = mapContainer.offsetWidth;
|
||||
mapContainer.style.height = width + 'px';
|
||||
|
||||
parkMap = L.map('park-map').setView([latitude, longitude], 13);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(parkMap);
|
||||
|
||||
L.marker([latitude, longitude])
|
||||
.addTo(parkMap)
|
||||
.bindPopup(name);
|
||||
|
||||
// Update map size when window is resized
|
||||
window.addEventListener('resize', function() {
|
||||
const width = mapContainer.offsetWidth;
|
||||
mapContainer.style.height = width + 'px';
|
||||
parkMap.invalidateSize();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ document.addEventListener('alpine:init', () => {
|
||||
uploadProgress: 0,
|
||||
error: null,
|
||||
showCaptionModal: false,
|
||||
editingPhoto: null,
|
||||
editingPhoto: { caption: '' },
|
||||
|
||||
get canAddMorePhotos() {
|
||||
return this.photos.length < maxFiles;
|
||||
@@ -234,7 +234,7 @@ document.addEventListener('alpine:init', () => {
|
||||
);
|
||||
|
||||
this.showCaptionModal = false;
|
||||
this.editingPhoto = null;
|
||||
this.editingPhoto = { caption: '' };
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to update caption';
|
||||
console.error('Caption update error:', err);
|
||||
|
||||
@@ -3,38 +3,54 @@
|
||||
|
||||
{% block title %}{{ park.name }} - ThrillWiki{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% if park.latitude and park.longitude %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('photoUploadModal', () => ({
|
||||
show: false,
|
||||
editingPhoto: { caption: '' }
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="container px-4 mx-auto">
|
||||
<!-- Header with Quick Facts -->
|
||||
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="mb-6 lg:mb-0 lg:mr-6 lg:flex-1">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1>
|
||||
{% if park.city or park.state or park.country %}
|
||||
{% spaceless %}
|
||||
<p class="mb-2 text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-1 fas fa-map-marker-alt"></i> {% if park.city %}{{ park.city }}{% endif %}{% if park.city and park.state %}, {% endif %}{% if park.state %}{{ park.state }}{% endif %}{% if park.country and park.state or park.city %}, {% endif %}{% if park.country %}{{ park.country }}{% endif %}
|
||||
</p>
|
||||
{% endspaceless %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Action Buttons - Above header -->
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex gap-2">
|
||||
<a href="{% url 'parks:park_update' park.slug %}" class="btn-secondary">
|
||||
<div class="flex justify-end gap-3 mb-4">
|
||||
<a href="{% url 'parks:park_update' park.slug %}"
|
||||
class="transition-transform btn-secondary hover:scale-105">
|
||||
<i class="mr-2 fas fa-pencil-alt"></i>Edit
|
||||
</a>
|
||||
{% if perms.media.add_photo %}
|
||||
<button class="btn-secondary" @click="$dispatch('show-photo-upload')">
|
||||
<button class="transition-transform btn-secondary hover:scale-105"
|
||||
@click="$dispatch('show-photo-upload')">
|
||||
<i class="mr-2 fas fa-camera"></i>Upload Photo
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Header Grid -->
|
||||
<div class="grid h-[340px] gap-4 mb-6 grid-cols-1 sm:grid-cols-6 md:grid-cols-12">
|
||||
<!-- Park Info Card -->
|
||||
<div class="flex flex-col h-full col-span-1 p-4 overflow-auto bg-white rounded-lg shadow-lg sm:col-span-2 md:col-span-3 dark:bg-gray-800">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1>
|
||||
|
||||
{% if park.formatted_location %}
|
||||
<div class="flex items-center mt-2 text-gray-600 dark:text-gray-400">
|
||||
<i class="mr-2 fas fa-map-marker-alt"></i>
|
||||
<p>{{ park.formatted_location }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 mt-3">
|
||||
<span class="status-badge text-sm font-medium {% if park.status == 'OPERATING' %}status-operating
|
||||
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
|
||||
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
|
||||
{% elif park.status == 'DEMOLISHED' %}status-demolished
|
||||
@@ -42,7 +58,7 @@
|
||||
{{ park.get_status_display }}
|
||||
</span>
|
||||
{% if park.average_rating %}
|
||||
<span class="flex items-center text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50">
|
||||
<span class="flex items-center text-sm font-medium text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50">
|
||||
<span class="mr-1 text-yellow-500 dark:text-yellow-200">★</span>
|
||||
{{ park.average_rating|floatformat:1 }}/10
|
||||
</span>
|
||||
@@ -50,67 +66,84 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Facts in Header -->
|
||||
<div class="lg:w-1/3">
|
||||
<dl class="grid grid-cols-2 gap-4">
|
||||
<!-- Stats and Quick Facts -->
|
||||
<div class="grid h-full grid-cols-1 col-span-1 gap-4 sm:grid-cols-6 sm:col-span-4 md:grid-cols-12 md:col-span-9">
|
||||
<!-- Stats Column -->
|
||||
<div class="flex flex-col col-span-1 gap-4 sm:col-span-2 md:col-span-4">
|
||||
<!-- Total Rides Card -->
|
||||
{% if park.total_rides %}
|
||||
<a href="{% url 'parks:rides:ride_list' park.slug %}"
|
||||
class="flex flex-col flex-1 p-4 transition-transform bg-white rounded-lg shadow-lg hover:scale-[1.02] dark:bg-gray-800">
|
||||
<dt class="text-lg font-semibold text-gray-900 dark:text-white">Total Rides</dt>
|
||||
<dd class="mt-2 text-4xl font-bold text-gray-900 dark:text-white">{{ park.total_rides }}</dd>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Total Roller Coasters Card -->
|
||||
{% if park.total_roller_coasters %}
|
||||
<div class="flex flex-col flex-1 p-4 bg-white rounded-lg shadow-lg dark:bg-gray-800">
|
||||
<dt class="text-lg font-semibold text-gray-900 dark:text-white">Total Roller Coasters</dt>
|
||||
<dd class="mt-2 text-4xl font-bold text-gray-900 dark:text-white">{{ park.total_roller_coasters }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Quick Facts Grid -->
|
||||
<div class="grid h-full grid-cols-2 col-span-1 gap-4 p-4 bg-white rounded-lg shadow-lg sm:grid-cols-2 sm:col-span-4 md:grid-cols-4 md:col-span-8 lg:grid-cols-6 dark:bg-gray-800">
|
||||
{% if park.owner %}
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500">Owner/Operator</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">
|
||||
<div class="flex flex-col items-center justify-center text-center lg:col-span-2">
|
||||
<i class="mb-1 text-lg text-blue-600 fas fa-building dark:text-blue-400"></i>
|
||||
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Owner/Operator</dt>
|
||||
<dd class="mt-0.5">
|
||||
<a href="{% url 'companies:company_detail' park.owner.slug %}"
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
{{ park.owner.name }}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if park.opening_date %}
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500">Opening Date</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{{ park.opening_date }}</dd>
|
||||
<div class="flex flex-col items-center justify-center text-center">
|
||||
<i class="mb-1 text-lg text-blue-600 fas fa-calendar-alt dark:text-blue-400"></i>
|
||||
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Opening Date</dt>
|
||||
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.opening_date }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if park.operating_season %}
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500">Operating Season</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{{ park.operating_season }}</dd>
|
||||
<div class="flex flex-col items-center justify-center text-center">
|
||||
<i class="mb-1 text-lg text-blue-600 fas fa-clock dark:text-blue-400"></i>
|
||||
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Operating Season</dt>
|
||||
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.operating_season }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if park.size_acres %}
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500">Size</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{{ park.size_acres }} acres</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if park.total_rides %}
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500">Total Rides</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{{ park.total_rides }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if park.total_roller_coasters %}
|
||||
<div>
|
||||
<dt class="text-sm text-gray-500">Roller Coasters</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{{ park.total_roller_coasters }}</dd>
|
||||
<div class="flex flex-col items-center justify-center text-center">
|
||||
<i class="mb-1 text-lg text-blue-600 fas fa-ruler-combined dark:text-blue-400"></i>
|
||||
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Size</dt>
|
||||
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.size_acres }} acres</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if park.website %}
|
||||
<div class="col-span-2">
|
||||
<dt class="text-sm text-gray-500">Website</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">
|
||||
<div class="flex flex-col items-center justify-center text-center lg:col-span-2">
|
||||
<i class="mb-1 text-lg text-blue-600 fas fa-globe dark:text-blue-400"></i>
|
||||
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Website</dt>
|
||||
<dd class="mt-0.5">
|
||||
<a href="{{ park.website }}"
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
class="inline-flex items-center text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
<i class="mr-2 fas fa-external-link-alt"></i>Official Website
|
||||
Official Website
|
||||
<i class="ml-1 fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photos -->
|
||||
{% if park.photos.exists %}
|
||||
<div class="p-6 mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
@@ -171,7 +204,7 @@
|
||||
{% if park.latitude and park.longitude %}
|
||||
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Location</h2>
|
||||
<div id="map" class="relative rounded-lg" style="z-index: 0;"></div>
|
||||
<div id="park-map" class="relative rounded-lg" style="z-index: 0;"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -189,10 +222,14 @@
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{% for field, changes in record.diff_against_previous.items %}
|
||||
{% if field != "updated_at" %}
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">{{ field }}:</span>
|
||||
{{ changes.old }} → {{ changes.new }}
|
||||
<span class="font-medium">{{ field|title }}:</span>
|
||||
<span class="text-red-600 dark:text-red-400">{{ changes.old }}</span>
|
||||
<span class="mx-1">→</span>
|
||||
<span class="text-green-600 dark:text-green-400">{{ changes.new }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,12 +240,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photo Upload Modal -->
|
||||
{% if perms.media.add_photo %}
|
||||
<div x-data="{ show: false }"
|
||||
@show-photo-upload.window="show = true"
|
||||
<div x-cloak
|
||||
x-data="{
|
||||
show: false,
|
||||
editingPhoto: null,
|
||||
init() {
|
||||
this.editingPhoto = { caption: '' };
|
||||
}
|
||||
}"
|
||||
@show-photo-upload.window="show = true; init()"
|
||||
x-show="show"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-black bg-opacity-50"
|
||||
@click.self="show = false">
|
||||
@@ -227,37 +270,13 @@
|
||||
{% if park.latitude and park.longitude %}
|
||||
{% block extra_js %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="{% static 'js/park-map.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Make map container square based on its width
|
||||
const mapContainer = document.getElementById('map');
|
||||
const width = mapContainer.offsetWidth;
|
||||
mapContainer.style.height = width + 'px';
|
||||
|
||||
const map = L.map('map').setView([{{ park.latitude }}, {{ park.longitude }}], 13);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
L.marker([{{ park.latitude }}, {{ park.longitude }}])
|
||||
.addTo(map)
|
||||
.bindPopup("{{ park.name }}");
|
||||
|
||||
// Update map size when window is resized
|
||||
window.addEventListener('resize', function() {
|
||||
const width = mapContainer.offsetWidth;
|
||||
mapContainer.style.height = width + 'px';
|
||||
map.invalidateSize();
|
||||
});
|
||||
initParkMap({{ park.latitude }}, {{ park.longitude }}, "{{ park.name }}");
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% if park.latitude and park.longitude %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user