mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:51:10 -05:00
weird header stuff
This commit is contained in:
@@ -7,14 +7,20 @@ class HistoryTrackingConfig(AppConfig):
|
|||||||
name = "history_tracking"
|
name = "history_tracking"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
from django.apps import apps
|
||||||
from .mixins import HistoricalChangeMixin
|
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:
|
# Apply mixin to historical models
|
||||||
# Check if mixin is already applied
|
if HistoricalChangeMixin not in Park.history.model.__bases__:
|
||||||
if HistoricalChangeMixin not in model.history.model.__bases__:
|
Park.history.model.__bases__ = (HistoricalChangeMixin,) + Park.history.model.__bases__
|
||||||
model.history.model.__bases__ = (
|
|
||||||
HistoricalChangeMixin,
|
if HistoricalChangeMixin not in ParkArea.history.model.__bases__:
|
||||||
) + model.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:
|
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
|
@property
|
||||||
def diff_against_previous(self):
|
def diff_against_previous(self):
|
||||||
prev_record = self.prev_record
|
prev_record = self.prev_record
|
||||||
@@ -15,9 +26,14 @@ class HistoricalChangeMixin:
|
|||||||
"history_id",
|
"history_id",
|
||||||
"history_type",
|
"history_type",
|
||||||
"history_user_id",
|
"history_user_id",
|
||||||
|
"history_change_reason",
|
||||||
|
"history_type",
|
||||||
] and not field.startswith("_"):
|
] and not field.startswith("_"):
|
||||||
|
try:
|
||||||
old_value = getattr(prev_record, field)
|
old_value = getattr(prev_record, field)
|
||||||
new_value = getattr(self, field)
|
new_value = getattr(self, field)
|
||||||
if old_value != new_value:
|
if old_value != new_value:
|
||||||
changes[field] = {"old": old_value, "new": new_value}
|
changes[field] = {"old": old_value, "new": new_value}
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
return changes
|
return changes
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ from simple_history.models import HistoricalRecords
|
|||||||
from .mixins import HistoricalChangeMixin
|
from .mixins import HistoricalChangeMixin
|
||||||
|
|
||||||
|
|
||||||
class Park(models.Model):
|
class HistoricalModel(models.Model):
|
||||||
name = models.CharField(max_length=200)
|
"""Abstract base class for models with history tracking"""
|
||||||
# ... other fields ...
|
class Meta:
|
||||||
history = HistoricalRecords()
|
abstract = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _history_model(self):
|
def _history_model(self):
|
||||||
return self.history.model
|
return self.history.model
|
||||||
|
|
||||||
|
|
||||||
# Apply the mixin
|
|
||||||
|
|||||||
@@ -202,8 +202,8 @@ class HistoryMixin:
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
|
||||||
# Get historical records
|
# Get historical records ordered by date
|
||||||
context['history'] = obj.history.all().select_related('history_user')
|
context['history'] = obj.history.all().select_related('history_user').order_by('-history_date')
|
||||||
|
|
||||||
# Get related edit submissions
|
# Get related edit submissions
|
||||||
content_type = ContentType.objects.get_for_model(obj)
|
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
|
# Register the models with their admin classes
|
||||||
admin.site.register(Park, ParkAdmin)
|
admin.site.register(Park, ParkAdmin)
|
||||||
admin.site.register(ParkArea, ParkAreaAdmin)
|
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'
|
name = 'parks'
|
||||||
|
|
||||||
def ready(self):
|
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 companies.models import Company
|
||||||
from media.models import Photo
|
from media.models import Photo
|
||||||
|
from history_tracking.models import HistoricalModel
|
||||||
|
|
||||||
|
|
||||||
def normalize_coordinate(value, max_digits, decimal_places):
|
def normalize_coordinate(value, max_digits, decimal_places):
|
||||||
@@ -53,7 +54,7 @@ def validate_longitude_digits(value):
|
|||||||
return validate_coordinate_digits(value, 10, 6)
|
return validate_coordinate_digits(value, 10, 6)
|
||||||
|
|
||||||
|
|
||||||
class Park(models.Model):
|
class Park(HistoricalModel):
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("OPERATING", "Operating"),
|
("OPERATING", "Operating"),
|
||||||
("CLOSED_TEMP", "Temporarily Closed"),
|
("CLOSED_TEMP", "Temporarily Closed"),
|
||||||
@@ -176,7 +177,7 @@ class Park(models.Model):
|
|||||||
raise cls.DoesNotExist()
|
raise cls.DoesNotExist()
|
||||||
|
|
||||||
|
|
||||||
class ParkArea(models.Model):
|
class ParkArea(HistoricalModel):
|
||||||
park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name="areas")
|
park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name="areas")
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
slug = models.SlugField(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;
|
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 {
|
.mx-1 {
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
@@ -2337,6 +2353,14 @@ select {
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-0\.5 {
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-8 {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -2405,6 +2429,10 @@ select {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-\[340px\] {
|
||||||
|
height: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
.max-h-60 {
|
.max-h-60 {
|
||||||
max-height: 15rem;
|
max-height: 15rem;
|
||||||
}
|
}
|
||||||
@@ -2421,6 +2449,10 @@ select {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-h-0 {
|
||||||
|
min-height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.w-16 {
|
.w-16 {
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
}
|
}
|
||||||
@@ -2547,6 +2579,14 @@ select {
|
|||||||
resize: both;
|
resize: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-rows-fr {
|
||||||
|
grid-auto-rows: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-rows-min {
|
||||||
|
grid-auto-rows: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-cols-1 {
|
.grid-cols-1 {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@@ -2559,6 +2599,14 @@ select {
|
|||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
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-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -2603,6 +2651,37 @@ select {
|
|||||||
gap: 1.5rem;
|
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]) {
|
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-space-x-reverse: 0;
|
--tw-space-x-reverse: 0;
|
||||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||||
@@ -3156,6 +3235,11 @@ select {
|
|||||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
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 {
|
||||||
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));
|
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 {
|
.hover\:border-gray-300:hover {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||||
@@ -3670,6 +3760,11 @@ select {
|
|||||||
color: rgb(254 252 232 / var(--tw-text-opacity));
|
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 *) {
|
.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-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);
|
--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) {
|
@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 {
|
.sm\:grid-cols-2 {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
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]) {
|
.sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-space-x-reverse: 0;
|
--tw-space-x-reverse: 0;
|
||||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||||
@@ -3762,6 +3869,26 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@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 {
|
.md\:mt-0 {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
@@ -3778,6 +3905,14 @@ select {
|
|||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
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 {
|
.md\:flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@@ -3786,6 +3921,10 @@ select {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md\:justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.md\:text-2xl {
|
.md\:text-2xl {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
@@ -3806,6 +3945,22 @@ select {
|
|||||||
grid-column: span 2 / span 2;
|
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 {
|
.lg\:mb-0 {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
@@ -3814,6 +3969,14 @@ select {
|
|||||||
margin-right: 1.5rem;
|
margin-right: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lg\:ml-8 {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg\:mt-0 {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.lg\:flex {
|
.lg\:flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@@ -3826,6 +3989,10 @@ select {
|
|||||||
width: 33.333333%;
|
width: 33.333333%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lg\:w-1\/2 {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.lg\:flex-1 {
|
.lg\:flex-1 {
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
}
|
}
|
||||||
@@ -3838,6 +4005,18 @@ select {
|
|||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
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 {
|
.lg\:flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@@ -3855,3 +4034,9 @@ select {
|
|||||||
line-height: 1;
|
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,
|
uploadProgress: 0,
|
||||||
error: null,
|
error: null,
|
||||||
showCaptionModal: false,
|
showCaptionModal: false,
|
||||||
editingPhoto: null,
|
editingPhoto: { caption: '' },
|
||||||
|
|
||||||
get canAddMorePhotos() {
|
get canAddMorePhotos() {
|
||||||
return this.photos.length < maxFiles;
|
return this.photos.length < maxFiles;
|
||||||
@@ -234,7 +234,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.showCaptionModal = false;
|
this.showCaptionModal = false;
|
||||||
this.editingPhoto = null;
|
this.editingPhoto = { caption: '' };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = err.message || 'Failed to update caption';
|
this.error = err.message || 'Failed to update caption';
|
||||||
console.error('Caption update error:', err);
|
console.error('Caption update error:', err);
|
||||||
|
|||||||
@@ -3,38 +3,54 @@
|
|||||||
|
|
||||||
{% block title %}{{ park.name }} - ThrillWiki{% endblock %}
|
{% 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 %}
|
{% block content %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('photoUploadModal', () => ({
|
||||||
|
show: false,
|
||||||
|
editingPhoto: { caption: '' }
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="container px-4 mx-auto">
|
<div class="container px-4 mx-auto">
|
||||||
<!-- Header with Quick Facts -->
|
<!-- Action Buttons - Above header -->
|
||||||
<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>
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<div class="flex gap-2">
|
<div class="flex justify-end gap-3 mb-4">
|
||||||
<a href="{% url 'parks:park_update' park.slug %}" class="btn-secondary">
|
<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
|
<i class="mr-2 fas fa-pencil-alt"></i>Edit
|
||||||
</a>
|
</a>
|
||||||
{% if perms.media.add_photo %}
|
{% 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
|
<i class="mr-2 fas fa-camera"></i>Upload Photo
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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>
|
||||||
<div class="flex flex-wrap gap-2 mt-3">
|
{% endif %}
|
||||||
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating
|
|
||||||
|
<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 == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
|
||||||
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
|
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
|
||||||
{% elif park.status == 'DEMOLISHED' %}status-demolished
|
{% elif park.status == 'DEMOLISHED' %}status-demolished
|
||||||
@@ -42,7 +58,7 @@
|
|||||||
{{ park.get_status_display }}
|
{{ park.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
{% if park.average_rating %}
|
{% 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>
|
<span class="mr-1 text-yellow-500 dark:text-yellow-200">★</span>
|
||||||
{{ park.average_rating|floatformat:1 }}/10
|
{{ park.average_rating|floatformat:1 }}/10
|
||||||
</span>
|
</span>
|
||||||
@@ -50,67 +66,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Facts in Header -->
|
<!-- Stats and Quick Facts -->
|
||||||
<div class="lg:w-1/3">
|
<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">
|
||||||
<dl class="grid grid-cols-2 gap-4">
|
<!-- 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 %}
|
{% if park.owner %}
|
||||||
<div>
|
<div class="flex flex-col items-center justify-center text-center lg:col-span-2">
|
||||||
<dt class="text-sm text-gray-500">Owner/Operator</dt>
|
<i class="mb-1 text-lg text-blue-600 fas fa-building dark:text-blue-400"></i>
|
||||||
<dd class="font-medium text-gray-900 dark:text-white">
|
<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 %}"
|
<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 }}
|
{{ park.owner.name }}
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if park.opening_date %}
|
{% if park.opening_date %}
|
||||||
<div>
|
<div class="flex flex-col items-center justify-center text-center">
|
||||||
<dt class="text-sm text-gray-500">Opening Date</dt>
|
<i class="mb-1 text-lg text-blue-600 fas fa-calendar-alt dark:text-blue-400"></i>
|
||||||
<dd class="font-medium text-gray-900 dark:text-white">{{ park.opening_date }}</dd>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if park.operating_season %}
|
{% if park.operating_season %}
|
||||||
<div>
|
<div class="flex flex-col items-center justify-center text-center">
|
||||||
<dt class="text-sm text-gray-500">Operating Season</dt>
|
<i class="mb-1 text-lg text-blue-600 fas fa-clock dark:text-blue-400"></i>
|
||||||
<dd class="font-medium text-gray-900 dark:text-white">{{ park.operating_season }}</dd>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if park.size_acres %}
|
{% if park.size_acres %}
|
||||||
<div>
|
<div class="flex flex-col items-center justify-center text-center">
|
||||||
<dt class="text-sm text-gray-500">Size</dt>
|
<i class="mb-1 text-lg text-blue-600 fas fa-ruler-combined dark:text-blue-400"></i>
|
||||||
<dd class="font-medium text-gray-900 dark:text-white">{{ park.size_acres }} acres</dd>
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Size</dt>
|
||||||
</div>
|
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.size_acres }} acres</dd>
|
||||||
{% 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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if park.website %}
|
{% if park.website %}
|
||||||
<div class="col-span-2">
|
<div class="flex flex-col items-center justify-center text-center lg:col-span-2">
|
||||||
<dt class="text-sm text-gray-500">Website</dt>
|
<i class="mb-1 text-lg text-blue-600 fas fa-globe dark:text-blue-400"></i>
|
||||||
<dd class="font-medium text-gray-900 dark:text-white">
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Website</dt>
|
||||||
|
<dd class="mt-0.5">
|
||||||
<a href="{{ park.website }}"
|
<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">
|
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>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Photos -->
|
<!-- Photos -->
|
||||||
{% if park.photos.exists %}
|
{% if park.photos.exists %}
|
||||||
<div class="p-6 mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
<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 %}
|
{% if park.latitude and park.longitude %}
|
||||||
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
<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>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -189,10 +222,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{% for field, changes in record.diff_against_previous.items %}
|
{% for field, changes in record.diff_against_previous.items %}
|
||||||
|
{% if field != "updated_at" %}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="font-medium">{{ field }}:</span>
|
<span class="font-medium">{{ field|title }}:</span>
|
||||||
{{ changes.old }} → {{ changes.new }}
|
<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>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,12 +240,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Photo Upload Modal -->
|
<!-- Photo Upload Modal -->
|
||||||
{% if perms.media.add_photo %}
|
{% if perms.media.add_photo %}
|
||||||
<div x-data="{ show: false }"
|
<div x-cloak
|
||||||
@show-photo-upload.window="show = true"
|
x-data="{
|
||||||
|
show: false,
|
||||||
|
editingPhoto: null,
|
||||||
|
init() {
|
||||||
|
this.editingPhoto = { caption: '' };
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@show-photo-upload.window="show = true; init()"
|
||||||
x-show="show"
|
x-show="show"
|
||||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-black bg-opacity-50"
|
class="fixed inset-0 z-[60] flex items-center justify-center bg-black bg-opacity-50"
|
||||||
@click.self="show = false">
|
@click.self="show = false">
|
||||||
@@ -227,37 +270,13 @@
|
|||||||
{% if park.latitude and park.longitude %}
|
{% if park.latitude and park.longitude %}
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="{% static 'js/park-map.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Make map container square based on its width
|
initParkMap({{ park.latitude }}, {{ park.longitude }}, "{{ park.name }}");
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endif %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user