weird header stuff

This commit is contained in:
pacnpal
2024-11-03 22:03:56 +00:00
parent ed585e6a56
commit 07526dcba8
34 changed files with 5047 additions and 185 deletions

View File

@@ -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:
for model in models_with_history: Park = apps.get_model('parks', 'Park')
# Check if mixin is already applied ParkArea = apps.get_model('parks', 'ParkArea')
if HistoricalChangeMixin not in model.history.model.__bases__:
model.history.model.__bases__ = ( # Apply mixin to historical models
HistoricalChangeMixin, if HistoricalChangeMixin not in Park.history.model.__bases__:
) + model.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

View File

@@ -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("_"):
old_value = getattr(prev_record, field) try:
new_value = getattr(self, field) old_value = getattr(prev_record, field)
if old_value != new_value: new_value = getattr(self, field)
changes[field] = {"old": old_value, "new": new_value} if old_value != new_value:
changes[field] = {"old": old_value, "new": new_value}
except AttributeError:
continue
return changes return changes

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

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

View 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

File diff suppressed because it is too large Load Diff

18
staticfiles/js/alerts.js Normal file
View 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

File diff suppressed because one or more lines are too long

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

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

View File

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

View File

@@ -3,114 +3,147 @@
{% 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"> {% if user.is_authenticated %}
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between"> <div class="flex justify-end gap-3 mb-4">
<div class="mb-6 lg:mb-0 lg:mr-6 lg:flex-1"> <a href="{% url 'parks:park_update' park.slug %}"
<div class="flex items-start justify-between"> class="transition-transform btn-secondary hover:scale-105">
<div> <i class="mr-2 fas fa-pencil-alt"></i>Edit
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1> </a>
{% if park.city or park.state or park.country %} {% if perms.media.add_photo %}
{% spaceless %} <button class="transition-transform btn-secondary hover:scale-105"
<p class="mb-2 text-gray-600 dark:text-gray-400"> @click="$dispatch('show-photo-upload')">
<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 %} <i class="mr-2 fas fa-camera"></i>Upload Photo
</p> </button>
{% endspaceless %} {% endif %}
{% endif %} </div>
</div> {% endif %}
{% if user.is_authenticated %}
<div class="flex gap-2"> <!-- Header Grid -->
<a href="{% url 'parks:park_update' park.slug %}" class="btn-secondary"> <div class="grid h-[340px] gap-4 mb-6 grid-cols-1 sm:grid-cols-6 md:grid-cols-12">
<i class="mr-2 fas fa-pencil-alt"></i>Edit <!-- Park Info Card -->
</a> <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">
{% if perms.media.add_photo %} <h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1>
<button class="btn-secondary" @click="$dispatch('show-photo-upload')">
<i class="mr-2 fas fa-camera"></i>Upload Photo {% if park.formatted_location %}
</button> <div class="flex items-center mt-2 text-gray-600 dark:text-gray-400">
{% endif %} <i class="mr-2 fas fa-map-marker-alt"></i>
</div> <p>{{ park.formatted_location }}</p>
{% endif %} </div>
</div> {% endif %}
<div class="flex flex-wrap gap-2 mt-3">
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating <div class="flex flex-wrap items-center gap-2 mt-3">
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed <span class="status-badge text-sm font-medium {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction {% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'DEMOLISHED' %}status-demolished {% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}"> {% elif park.status == 'DEMOLISHED' %}status-demolished
{{ park.get_status_display }} {% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
</span> {{ park.get_status_display }}
{% if park.average_rating %} </span>
<span class="flex items-center text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50"> {% if park.average_rating %}
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span> <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">
{{ park.average_rating|floatformat:1 }}/10 <span class="mr-1 text-yellow-500 dark:text-yellow-200"></span>
</span> {{ park.average_rating|floatformat:1 }}/10
{% endif %} </span>
{% endif %}
</div>
</div>
<!-- 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> </div>
{% endif %}
</div> </div>
<!-- Quick Facts in Header --> <!-- Quick Facts Grid -->
<div class="lg:w-1/3"> <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">
<dl class="grid grid-cols-2 gap-4"> {% if park.owner %}
{% if park.owner %} <div class="flex flex-col items-center justify-center text-center lg:col-span-2">
<div> <i class="mb-1 text-lg text-blue-600 fas fa-building dark:text-blue-400"></i>
<dt class="text-sm text-gray-500">Owner/Operator</dt> <dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Owner/Operator</dt>
<dd class="font-medium text-gray-900 dark:text-white"> <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 %}
<div> {% if park.opening_date %}
<dt class="text-sm text-gray-500">Opening Date</dt> <div class="flex flex-col items-center justify-center text-center">
<dd class="font-medium text-gray-900 dark:text-white">{{ park.opening_date }}</dd> <i class="mb-1 text-lg text-blue-600 fas fa-calendar-alt dark:text-blue-400"></i>
</div> <dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Opening Date</dt>
{% endif %} <dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.opening_date }}</dd>
{% if park.operating_season %} </div>
<div> {% endif %}
<dt class="text-sm text-gray-500">Operating Season</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ park.operating_season }}</dd> {% if park.operating_season %}
</div> <div class="flex flex-col items-center justify-center text-center">
{% endif %} <i class="mb-1 text-lg text-blue-600 fas fa-clock dark:text-blue-400"></i>
{% if park.size_acres %} <dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Operating Season</dt>
<div> <dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.operating_season }}</dd>
<dt class="text-sm text-gray-500">Size</dt> </div>
<dd class="font-medium text-gray-900 dark:text-white">{{ park.size_acres }} acres</dd> {% endif %}
</div>
{% endif %} {% if park.size_acres %}
{% if park.total_rides %} <div class="flex flex-col items-center justify-center text-center">
<div> <i class="mb-1 text-lg text-blue-600 fas fa-ruler-combined dark:text-blue-400"></i>
<dt class="text-sm text-gray-500">Total Rides</dt> <dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Size</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ park.total_rides }}</dd> <dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.size_acres }} acres</dd>
</div> </div>
{% endif %} {% endif %}
{% if park.total_roller_coasters %}
<div> {% if park.website %}
<dt class="text-sm text-gray-500">Roller Coasters</dt> <div class="flex flex-col items-center justify-center text-center lg:col-span-2">
<dd class="font-medium text-gray-900 dark:text-white">{{ park.total_roller_coasters }}</dd> <i class="mb-1 text-lg text-blue-600 fas fa-globe dark:text-blue-400"></i>
</div> <dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Website</dt>
{% endif %} <dd class="mt-0.5">
{% if park.website %} <a href="{{ park.website }}"
<div class="col-span-2"> class="inline-flex items-center text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
<dt class="text-sm text-gray-500">Website</dt> target="_blank" rel="noopener noreferrer">
<dd class="font-medium text-gray-900 dark:text-white"> Official Website
<a href="{{ park.website }}" <i class="ml-1 fas fa-external-link-alt"></i>
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" </a>
target="_blank" rel="noopener noreferrer"> </dd>
<i class="mr-2 fas fa-external-link-alt"></i>Official Website </div>
</a> {% endif %}
</dd>
</div>
{% 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 %}
<div class="text-sm"> {% if field != "updated_at" %}
<span class="font-medium">{{ field }}:</span> <div class="text-sm">
{{ changes.old }} → {{ changes.new }} <span class="font-medium">{{ field|title }}:</span>
</div> <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 %} {% 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 %}