add category views for each type of ride, add ride designers

This commit is contained in:
pacnpal
2024-11-04 00:33:19 +00:00
parent 07526dcba8
commit ae913e757a
35 changed files with 1607 additions and 275 deletions

View File

@@ -5,6 +5,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib import messages from django.contrib import messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.db.models import Count, Sum
from .models import Company, Manufacturer from .models import Company, Manufacturer
from .forms import CompanyForm, ManufacturerForm from .forms import CompanyForm, ManufacturerForm
from rides.models import Ride from rides.models import Ride
@@ -173,9 +174,13 @@ class CompanyDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionM
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['parks'] = Park.objects.filter( parks = Park.objects.filter(
owner=self.object owner=self.object
).select_related('owner') ).select_related('owner')
context['parks'] = parks
context['total_rides'] = Ride.objects.filter(park__in=parks).count()
return context return context
def get_redirect_url_pattern(self): def get_redirect_url_pattern(self):
@@ -195,9 +200,14 @@ class ManufacturerDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmis
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['rides'] = Ride.objects.filter( rides = Ride.objects.filter(
manufacturer=self.object manufacturer=self.object
).select_related('park', 'coaster_stats') ).select_related('park', 'coaster_stats')
context['rides'] = rides
context['coaster_count'] = rides.filter(category='ROLLER_COASTER').count()
context['parks_count'] = rides.values('park').distinct().count()
return context return context
def get_redirect_url_pattern(self): def get_redirect_url_pattern(self):

0
designers/__init__.py Normal file
View File

10
designers/admin.py Normal file
View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from .models import Designer
@admin.register(Designer)
class DesignerAdmin(SimpleHistoryAdmin):
list_display = ('name', 'headquarters', 'founded_date', 'website')
search_fields = ('name', 'headquarters')
list_filter = ('founded_date',)
prepopulated_fields = {'slug': ('name',)}

6
designers/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class DesignersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "designers"

View File

@@ -0,0 +1,88 @@
# Generated by Django 5.1.2 on 2024-11-04 00:28
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Designer",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("slug", models.SlugField(max_length=255, unique=True)),
("description", models.TextField(blank=True)),
("website", models.URLField(blank=True)),
("founded_date", models.DateField(blank=True, null=True)),
("headquarters", models.CharField(blank=True, max_length=255)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="HistoricalDesigner",
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)),
("website", models.URLField(blank=True)),
("founded_date", models.DateField(blank=True, null=True)),
("headquarters", models.CharField(blank=True, max_length=255)),
("created_at", models.DateTimeField(blank=True, editable=False)),
("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,
),
),
],
options={
"verbose_name": "historical designer",
"verbose_name_plural": "historical designers",
"ordering": ("-history_date", "-history_id"),
"get_latest_by": ("history_date", "history_id"),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]

View File

37
designers/models.py Normal file
View File

@@ -0,0 +1,37 @@
from django.db import models
from django.utils.text import slugify
from simple_history.models import HistoricalRecords
class Designer(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
description = models.TextField(blank=True)
website = models.URLField(blank=True)
founded_date = models.DateField(null=True, blank=True)
headquarters = models.CharField(max_length=255, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
history = HistoricalRecords()
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
@classmethod
def get_by_slug(cls, slug):
"""Get designer by current or historical slug"""
try:
return cls.objects.get(slug=slug), False
except cls.DoesNotExist:
# Check historical slugs
history = cls.history.filter(slug=slug).order_by('-history_date').first()
if history:
return cls.objects.get(id=history.id), True
raise cls.DoesNotExist("No designer found with this slug")

3
designers/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
designers/urls.py Normal file
View File

@@ -0,0 +1,8 @@
from django.urls import path
from . import views
app_name = 'designers'
urlpatterns = [
path('<slug:slug>/', views.DesignerDetailView.as_view(), name='designer_detail'),
]

29
designers/views.py Normal file
View File

@@ -0,0 +1,29 @@
from django.views.generic import DetailView
from .models import Designer
from django.db.models import Count
class DesignerDetailView(DetailView):
model = Designer
template_name = 'designers/designer_detail.html'
context_object_name = 'designer'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Get all rides by this designer
context['rides'] = self.object.rides.select_related(
'park',
'manufacturer',
'coaster_stats'
).order_by('-opening_date')
# Get stats
context['stats'] = {
'total_rides': self.object.rides.count(),
'total_parks': self.object.rides.values('park').distinct().count(),
'total_coasters': self.object.rides.filter(category='RC').count(),
'total_countries': self.object.rides.values(
'park__location__country'
).distinct().count(),
}
return context

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2024-11-04 00:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("history_tracking", "0001_initial"),
]
operations = [
migrations.RemoveField(
model_name="historicalpark",
name="history_user",
),
migrations.DeleteModel(
name="Park",
),
migrations.DeleteModel(
name="HistoricalPark",
),
]

View File

@@ -1,5 +1,6 @@
from django.urls import path, include from django.urls import path, include
from . import views from . import views
from rides.views import ParkSingleCategoryListView
app_name = "parks" app_name = "parks"
@@ -17,6 +18,14 @@ urlpatterns = [
# Area views # Area views
path("<slug:park_slug>/areas/<slug:area_slug>/", views.ParkAreaDetailView.as_view(), name="area_detail"), path("<slug:park_slug>/areas/<slug:area_slug>/", views.ParkAreaDetailView.as_view(), name="area_detail"),
# Park-specific category URLs
path("<slug:park_slug>/roller_coasters/", ParkSingleCategoryListView.as_view(), {'category': 'RC'}, name="park_roller_coasters"),
path("<slug:park_slug>/dark_rides/", ParkSingleCategoryListView.as_view(), {'category': 'DR'}, name="park_dark_rides"),
path("<slug:park_slug>/flat_rides/", ParkSingleCategoryListView.as_view(), {'category': 'FR'}, name="park_flat_rides"),
path("<slug:park_slug>/water_rides/", ParkSingleCategoryListView.as_view(), {'category': 'WR'}, name="park_water_rides"),
path("<slug:park_slug>/transports/", ParkSingleCategoryListView.as_view(), {'category': 'TR'}, name="park_transports"),
path("<slug:park_slug>/others/", ParkSingleCategoryListView.as_view(), {'category': 'OT'}, name="park_others"),
# Include rides URLs # Include rides URLs
path("<slug:park_slug>/rides/", include("rides.urls", namespace="rides")), path("<slug:park_slug>/rides/", include("rides.urls", namespace="rides")),
] ]

View File

@@ -4,7 +4,7 @@ from .models import Ride
class RideForm(forms.ModelForm): class RideForm(forms.ModelForm):
class Meta: class Meta:
model = Ride model = Ride
fields = ['name', 'park_area', 'category', 'manufacturer', 'model_name', 'status', fields = ['name', 'park_area', 'category', 'manufacturer', 'designer', 'model_name', 'status',
'opening_date', 'closing_date', 'status_since', 'min_height_in', 'max_height_in', 'opening_date', 'closing_date', 'status_since', 'min_height_in', 'max_height_in',
'accessibility_options', 'capacity_per_hour', 'ride_duration_seconds', 'description'] 'accessibility_options', 'capacity_per_hour', 'ride_duration_seconds', 'description']
widgets = { widgets = {
@@ -20,6 +20,9 @@ class RideForm(forms.ModelForm):
'manufacturer': forms.Select(attrs={ 'manufacturer': forms.Select(attrs={
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white' 'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
}), }),
'designer': forms.Select(attrs={
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
}),
'model_name': forms.TextInput(attrs={ 'model_name': forms.TextInput(attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white' 'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
}), }),

View File

@@ -0,0 +1,65 @@
# Generated by Django 5.1.2 on 2024-11-04 00:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("rides", "0002_alter_ride_manufacturer"),
]
operations = [
migrations.AddField(
model_name="historicalrollercoasterstats",
name="max_drop_height_ft",
field=models.DecimalField(
blank=True,
decimal_places=2,
help_text="Maximum vertical drop height in feet",
max_digits=6,
null=True,
),
),
migrations.AddField(
model_name="historicalrollercoasterstats",
name="track_material",
field=models.CharField(
blank=True,
choices=[
("STEEL", "Steel"),
("WOOD", "Wood"),
("HYBRID", "Hybrid"),
("OTHER", "Other"),
],
default="STEEL",
max_length=20,
),
),
migrations.AddField(
model_name="rollercoasterstats",
name="max_drop_height_ft",
field=models.DecimalField(
blank=True,
decimal_places=2,
help_text="Maximum vertical drop height in feet",
max_digits=6,
null=True,
),
),
migrations.AddField(
model_name="rollercoasterstats",
name="track_material",
field=models.CharField(
blank=True,
choices=[
("STEEL", "Steel"),
("WOOD", "Wood"),
("HYBRID", "Hybrid"),
("OTHER", "Other"),
],
default="STEEL",
max_length=20,
),
),
]

View File

@@ -0,0 +1,69 @@
# Generated by Django 5.1.2 on 2024-11-04 00:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("rides", "0003_historicalrollercoasterstats_max_drop_height_ft_and_more"),
]
operations = [
migrations.AddField(
model_name="historicalrollercoasterstats",
name="roller_coaster_type",
field=models.CharField(
blank=True,
choices=[
("SITDOWN", "Sit-Down"),
("INVERTED", "Inverted"),
("FLYING", "Flying"),
("STANDUP", "Stand-Up"),
("WING", "Wing"),
("SUSPENDED", "Suspended"),
("BOBSLED", "Bobsled"),
("PIPELINE", "Pipeline"),
("MOTORBIKE", "Motorbike"),
("FLOORLESS", "Floorless"),
("DIVE", "Dive"),
("FAMILY", "Family"),
("WILD_MOUSE", "Wild Mouse"),
("SPINNING", "Spinning"),
("FOURTH_DIMENSION", "4th Dimension"),
("OTHER", "Other"),
],
default="SITDOWN",
help_text="The type/style of roller coaster (e.g. Sit-Down, Inverted, Flying)",
max_length=20,
),
),
migrations.AddField(
model_name="rollercoasterstats",
name="roller_coaster_type",
field=models.CharField(
blank=True,
choices=[
("SITDOWN", "Sit-Down"),
("INVERTED", "Inverted"),
("FLYING", "Flying"),
("STANDUP", "Stand-Up"),
("WING", "Wing"),
("SUSPENDED", "Suspended"),
("BOBSLED", "Bobsled"),
("PIPELINE", "Pipeline"),
("MOTORBIKE", "Motorbike"),
("FLOORLESS", "Floorless"),
("DIVE", "Dive"),
("FAMILY", "Family"),
("WILD_MOUSE", "Wild Mouse"),
("SPINNING", "Spinning"),
("FOURTH_DIMENSION", "4th Dimension"),
("OTHER", "Other"),
],
default="SITDOWN",
help_text="The type/style of roller coaster (e.g. Sit-Down, Inverted, Flying)",
max_length=20,
),
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 5.1.2 on 2024-11-04 00:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("designers", "0001_initial"),
("rides", "0004_historicalrollercoasterstats_roller_coaster_type_and_more"),
]
operations = [
migrations.AddField(
model_name="historicalride",
name="designer",
field=models.ForeignKey(
blank=True,
db_constraint=False,
help_text="The designer/engineering firm responsible for the ride",
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="designers.designer",
),
),
migrations.AddField(
model_name="ride",
name="designer",
field=models.ForeignKey(
blank=True,
help_text="The designer/engineering firm responsible for the ride",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rides",
to="designers.designer",
),
),
]

View File

@@ -43,10 +43,19 @@ class Ride(models.Model):
default='OT' default='OT'
) )
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
'companies.manufacturer', on_delete=models.CASCADE, null=False, blank=False 'companies.manufacturer',
) on_delete=models.CASCADE,
# other fields... null=False,
blank=False
)
designer = models.ForeignKey(
'designers.Designer',
on_delete=models.SET_NULL,
related_name='rides',
null=True,
blank=True,
help_text='The designer/engineering firm responsible for the ride'
)
model_name = models.CharField(max_length=255, blank=True) model_name = models.CharField(max_length=255, blank=True)
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
@@ -108,6 +117,32 @@ class RollerCoasterStats(models.Model):
('OTHER', 'Other'), ('OTHER', 'Other'),
] ]
TRACK_MATERIAL_CHOICES = [
('STEEL', 'Steel'),
('WOOD', 'Wood'),
('HYBRID', 'Hybrid'),
('OTHER', 'Other'),
]
COASTER_TYPE_CHOICES = [
('SITDOWN', 'Sit-Down'),
('INVERTED', 'Inverted'),
('FLYING', 'Flying'),
('STANDUP', 'Stand-Up'),
('WING', 'Wing'),
('SUSPENDED', 'Suspended'),
('BOBSLED', 'Bobsled'),
('PIPELINE', 'Pipeline'),
('MOTORBIKE', 'Motorbike'),
('FLOORLESS', 'Floorless'),
('DIVE', 'Dive'),
('FAMILY', 'Family'),
('WILD_MOUSE', 'Wild Mouse'),
('SPINNING', 'Spinning'),
('FOURTH_DIMENSION', '4th Dimension'),
('OTHER', 'Other'),
]
ride = models.OneToOneField( ride = models.OneToOneField(
Ride, Ride,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -134,6 +169,26 @@ class RollerCoasterStats(models.Model):
inversions = models.PositiveIntegerField(default=0) inversions = models.PositiveIntegerField(default=0)
ride_time_seconds = models.PositiveIntegerField(null=True, blank=True) ride_time_seconds = models.PositiveIntegerField(null=True, blank=True)
track_type = models.CharField(max_length=255, blank=True) track_type = models.CharField(max_length=255, blank=True)
track_material = models.CharField(
max_length=20,
choices=TRACK_MATERIAL_CHOICES,
default='STEEL',
blank=True
)
roller_coaster_type = models.CharField(
max_length=20,
choices=COASTER_TYPE_CHOICES,
default='SITDOWN',
blank=True,
help_text='The type/style of roller coaster (e.g. Sit-Down, Inverted, Flying)'
)
max_drop_height_ft = models.DecimalField(
max_digits=6,
decimal_places=2,
null=True,
blank=True,
help_text='Maximum vertical drop height in feet'
)
launch_type = models.CharField( launch_type = models.CharField(
max_length=20, max_length=20,
choices=LAUNCH_CHOICES, choices=LAUNCH_CHOICES,

View File

@@ -4,8 +4,17 @@ from . import views
app_name = 'rides' # Add namespace app_name = 'rides' # Add namespace
urlpatterns = [ urlpatterns = [
path('all/', views.RideListView.as_view(), name='all_rides'), # New pattern for all rides # Global category URLs
path('', views.RideListView.as_view(), name='ride_list'), path('', views.RideListView.as_view(), name='ride_list'),
path('all/', views.RideListView.as_view(), name='all_rides'),
path('roller_coasters/', views.SingleCategoryListView.as_view(), {'category': 'RC'}, name='roller_coasters'),
path('dark_rides/', views.SingleCategoryListView.as_view(), {'category': 'DR'}, name='dark_rides'),
path('flat_rides/', views.SingleCategoryListView.as_view(), {'category': 'FR'}, name='flat_rides'),
path('water_rides/', views.SingleCategoryListView.as_view(), {'category': 'WR'}, name='water_rides'),
path('transports/', views.SingleCategoryListView.as_view(), {'category': 'TR'}, name='transports'),
path('others/', views.SingleCategoryListView.as_view(), {'category': 'OT'}, name='others'),
# Basic ride URLs
path('create/', views.RideCreateView.as_view(), name='ride_create'), path('create/', views.RideCreateView.as_view(), name='ride_create'),
path('<slug:ride_slug>/edit/', views.RideUpdateView.as_view(), name='ride_edit'), path('<slug:ride_slug>/edit/', views.RideUpdateView.as_view(), name='ride_edit'),
path('<slug:ride_slug>/', views.RideDetailView.as_view(), name='ride_detail'), path('<slug:ride_slug>/', views.RideDetailView.as_view(), name='ride_detail'),

View File

@@ -6,15 +6,79 @@ from django.db.models import Q
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib import messages from django.contrib import messages
from django.http import JsonResponse, HttpResponseRedirect from django.http import JsonResponse, HttpResponseRedirect, Http404
from django.db.models import Count
from .models import Ride, RollerCoasterStats from .models import Ride, RollerCoasterStats
from .forms import RideForm from .forms import RideForm
from parks.models import Park from parks.models import Park
from core.views import SlugRedirectMixin from core.views import SlugRedirectMixin
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin
from moderation.models import EditSubmission
from media.models import Photo from media.models import Photo
class SingleCategoryListView(ListView):
model = Ride
template_name = 'rides/ride_category_list.html'
context_object_name = 'categories'
def get_category_code(self):
category = self.kwargs.get('category')
if not category:
raise Http404("Category not found")
return category
def get_queryset(self):
category_code = self.get_category_code()
category_name = dict(Ride.CATEGORY_CHOICES)[category_code]
rides = Ride.objects.filter(category=category_code).select_related(
'park', 'manufacturer'
).order_by('name')
return {category_name: rides} if rides.exists() else {}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
category_code = self.get_category_code()
category_name = dict(Ride.CATEGORY_CHOICES)[category_code]
context['title'] = f'All {category_name}s'
context['category_code'] = category_code
return context
class ParkSingleCategoryListView(ListView):
model = Ride
template_name = 'rides/ride_category_list.html'
context_object_name = 'categories'
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
def get_category_code(self):
category = self.kwargs.get('category')
if not category:
raise Http404("Category not found")
return category
def get_queryset(self):
category_code = self.get_category_code()
category_name = dict(Ride.CATEGORY_CHOICES)[category_code]
rides = Ride.objects.filter(
park=self.park,
category=category_code
).select_related('manufacturer').order_by('name')
return {category_name: rides} if rides.exists() else {}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['park'] = self.park
category_code = self.get_category_code()
category_name = dict(Ride.CATEGORY_CHOICES)[category_code]
context['title'] = f'{category_name}s at {self.park.name}'
context['category_code'] = category_code
return context
class RideCreateView(LoginRequiredMixin, CreateView): class RideCreateView(LoginRequiredMixin, CreateView):
model = Ride model = Ride
form_class = RideForm form_class = RideForm

View File

@@ -2253,6 +2253,10 @@ select {
grid-column: span 1 / span 1; grid-column: span 1 / span 1;
} }
.col-span-12 {
grid-column: span 12 / span 12;
}
.mx-1 { .mx-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
margin-right: 0.25rem; margin-right: 0.25rem;
@@ -2361,6 +2365,30 @@ select {
margin-top: 2rem; margin-top: 2rem;
} }
.mr-1\.5 {
margin-right: 0.375rem;
}
.mb-0\.5 {
margin-bottom: 0.125rem;
}
.ml-0\.5 {
margin-left: 0.125rem;
}
.mr-0\.5 {
margin-right: 0.125rem;
}
.mt-1\.5 {
margin-top: 0.375rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.block { .block {
display: block; display: block;
} }
@@ -2433,6 +2461,10 @@ select {
height: 340px; height: 340px;
} }
.h-auto {
height: auto;
}
.max-h-60 { .max-h-60 {
max-height: 15rem; max-height: 15rem;
} }
@@ -2441,6 +2473,10 @@ select {
max-height: 90vh; max-height: 90vh;
} }
.max-h-\[340px\] {
max-height: 340px;
}
.min-h-\[calc\(100vh-16rem\)\] { .min-h-\[calc\(100vh-16rem\)\] {
min-height: calc(100vh - 16rem); min-height: calc(100vh - 16rem);
} }
@@ -2453,6 +2489,10 @@ select {
min-height: 0px; min-height: 0px;
} }
.min-h-\[200px\] {
min-height: 200px;
}
.w-16 { .w-16 {
width: 4rem; width: 4rem;
} }
@@ -2526,6 +2566,10 @@ select {
flex: 1 1 0%; flex: 1 1 0%;
} }
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow { .flex-grow {
flex-grow: 1; flex-grow: 1;
} }
@@ -2659,6 +2703,14 @@ select {
gap: 2rem; gap: 2rem;
} }
.gap-1\.5 {
gap: 0.375rem;
}
.gap-1 {
gap: 0.25rem;
}
.gap-x-8 { .gap-x-8 {
-moz-column-gap: 2rem; -moz-column-gap: 2rem;
column-gap: 2rem; column-gap: 2rem;
@@ -2732,6 +2784,10 @@ select {
overflow: hidden; overflow: hidden;
} }
.overflow-y-auto {
overflow-y: auto;
}
.rounded { .rounded {
border-radius: 0.25rem; border-radius: 0.25rem;
} }
@@ -2935,6 +2991,11 @@ select {
background-color: rgb(202 138 4 / var(--tw-bg-opacity)); background-color: rgb(202 138 4 / var(--tw-bg-opacity));
} }
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.bg-opacity-50 { .bg-opacity-50 {
--tw-bg-opacity: 0.5; --tw-bg-opacity: 0.5;
} }
@@ -3006,6 +3067,18 @@ select {
padding: 2rem; padding: 2rem;
} }
.p-2\.5 {
padding: 0.625rem;
}
.p-0\.5 {
padding: 0.125rem;
}
.p-1\.5 {
padding: 0.375rem;
}
.px-2 { .px-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
padding-right: 0.5rem; padding-right: 0.5rem;
@@ -3071,6 +3144,11 @@ select {
padding-bottom: 2rem; padding-bottom: 2rem;
} }
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.pb-4 { .pb-4 {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
@@ -3079,6 +3157,10 @@ select {
text-align: center; text-align: center;
} }
.align-middle {
vertical-align: middle;
}
.text-2xl { .text-2xl {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
@@ -3114,6 +3196,11 @@ select {
line-height: 1rem; line-height: 1rem;
} }
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
@@ -3126,6 +3213,10 @@ select {
font-weight: 600; font-weight: 600;
} }
.leading-tight {
line-height: 1.25;
}
.text-blue-500 { .text-blue-500 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity)); color: rgb(59 130 246 / var(--tw-text-opacity));
@@ -3240,6 +3331,16 @@ select {
color: rgb(22 163 74 / var(--tw-text-opacity)); color: rgb(22 163 74 / var(--tw-text-opacity));
} }
.text-sky-400 {
--tw-text-opacity: 1;
color: rgb(56 189 248 / var(--tw-text-opacity));
}
.text-sky-900 {
--tw-text-opacity: 1;
color: rgb(12 74 110 / var(--tw-text-opacity));
}
.opacity-0 { .opacity-0 {
opacity: 0; opacity: 0;
} }
@@ -3335,6 +3436,12 @@ select {
transition-duration: 150ms; transition-duration: 150ms;
} }
.transition-shadow {
transition-property: box-shadow;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-100 { .duration-100 {
transition-duration: 100ms; transition-duration: 100ms;
} }
@@ -3455,6 +3562,11 @@ select {
background-color: rgb(202 138 4 / var(--tw-bg-opacity)); background-color: rgb(202 138 4 / var(--tw-bg-opacity));
} }
.hover\:bg-gray-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.hover\:text-blue-500:hover { .hover\:text-blue-500:hover {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity)); color: rgb(59 130 246 / var(--tw-text-opacity));
@@ -3494,10 +3606,41 @@ select {
color: rgb(79 70 229 / 0.8); color: rgb(79 70 229 / 0.8);
} }
.hover\:text-sky-300:hover {
--tw-text-opacity: 1;
color: rgb(125 211 252 / var(--tw-text-opacity));
}
.hover\:text-sky-900:hover {
--tw-text-opacity: 1;
color: rgb(12 74 110 / var(--tw-text-opacity));
}
.hover\:text-sky-950:hover {
--tw-text-opacity: 1;
color: rgb(8 47 73 / var(--tw-text-opacity));
}
.hover\:text-sky-800:hover {
--tw-text-opacity: 1;
color: rgb(7 89 133 / var(--tw-text-opacity));
}
.hover\:text-blue-800:hover {
--tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity));
}
.hover\:underline:hover { .hover\:underline:hover {
text-decoration-line: underline; text-decoration-line: underline;
} }
.hover\:shadow-md:hover {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.focus\:border-blue-500:focus { .focus\:border-blue-500:focus {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity)); border-color: rgb(59 130 246 / var(--tw-border-opacity));
@@ -3765,6 +3908,11 @@ select {
color: rgb(74 222 128 / var(--tw-text-opacity)); color: rgb(74 222 128 / var(--tw-text-opacity));
} }
.dark\:text-sky-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(56 189 248 / 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);
@@ -3838,6 +3986,26 @@ select {
color: rgb(79 70 229 / var(--tw-text-opacity)); color: rgb(79 70 229 / var(--tw-text-opacity));
} }
.dark\:hover\:text-sky-400:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(56 189 248 / var(--tw-text-opacity));
}
.dark\:hover\:text-sky-600:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(2 132 199 / var(--tw-text-opacity));
}
.dark\:hover\:text-sky-200:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(186 230 253 / var(--tw-text-opacity));
}
.dark\:hover\:text-sky-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(125 211 252 / var(--tw-text-opacity));
}
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:col-span-2 { .sm\:col-span-2 {
grid-column: span 2 / span 2; grid-column: span 2 / span 2;
@@ -3847,6 +4015,54 @@ select {
grid-column: span 4 / span 4; grid-column: span 4 / span 4;
} }
.sm\:col-span-3 {
grid-column: span 3 / span 3;
}
.sm\:col-span-8 {
grid-column: span 8 / span 8;
}
.sm\:col-span-9 {
grid-column: span 9 / span 9;
}
.sm\:mb-8 {
margin-bottom: 2rem;
}
.sm\:mb-16 {
margin-bottom: 4rem;
}
.sm\:flex {
display: flex;
}
.sm\:h-\[340px\] {
height: 340px;
}
.sm\:h-\[300px\] {
height: 300px;
}
.sm\:h-\[200px\] {
height: 200px;
}
.sm\:h-\[160px\] {
height: 160px;
}
.sm\:h-\[140px\] {
height: 140px;
}
.sm\:h-auto {
height: auto;
}
.sm\:grid-cols-2 { .sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@@ -3855,6 +4071,26 @@ select {
grid-template-columns: repeat(6, minmax(0, 1fr)); grid-template-columns: repeat(6, minmax(0, 1fr));
} }
.sm\:grid-cols-12 {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.sm\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.sm\:grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.sm\:flex-col {
flex-direction: column;
}
.sm\:gap-4 {
gap: 1rem;
}
.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));
@@ -3866,6 +4102,41 @@ select {
margin-right: calc(1.5rem * var(--tw-space-x-reverse)); margin-right: calc(1.5rem * var(--tw-space-x-reverse));
margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse))); margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
} }
.sm\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.sm\:text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.sm\:text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.sm\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.sm\:text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.sm\:text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.sm\:text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
} }
@media (min-width: 768px) { @media (min-width: 768px) {
@@ -3893,6 +4164,14 @@ select {
margin-top: 0px; margin-top: 0px;
} }
.md\:mb-8 {
margin-bottom: 2rem;
}
.md\:h-\[140px\] {
height: 140px;
}
.md\:grid-cols-2 { .md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }

View File

@@ -5,75 +5,100 @@
{% block content %} {% block content %}
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto">
<!-- Company Header --> <!-- Action Buttons - Above header -->
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800"> <div class="flex justify-end gap-2 mb-2">
<div class="flex flex-col items-start justify-between md:flex-row md:items-center"> {% if company.website %}
<div> <a href="{{ company.website }}" target="_blank" rel="noopener noreferrer"
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ company.name }}</h1> class="transition-transform btn-secondary hover:scale-105">
{% if company.headquarters %} <i class="mr-1 fas fa-external-link-alt"></i>Visit Website
<p class="text-gray-600 dark:text-gray-400"> </a>
<i class="mr-2 fas fa-map-marker-alt"></i>{{ company.headquarters }} {% endif %}
</p> {% if user.is_authenticated %}
{% endif %} <a href="{% url 'companies:company_edit' slug=company.slug %}"
</div> class="transition-transform btn-secondary hover:scale-105">
<div class="flex gap-2 mt-4 md:mt-0"> <i class="mr-1 fas fa-edit"></i>Edit
{% if company.website %} </a>
<a href="{{ company.website }}" target="_blank" rel="noopener noreferrer"
class="btn-secondary">
<i class="mr-2 fas fa-external-link-alt"></i>Visit Website
</a>
{% endif %}
{% if user.is_authenticated %}
<a href="{% url 'companies:company_edit' slug=company.slug %}" class="btn-secondary">
<i class="mr-2 fas fa-edit"></i>Edit
</a>
{% endif %}
</div>
</div>
{% if company.description %}
<div class="mt-6 prose dark:prose-invert max-w-none">
{{ company.description|linebreaks }}
</div>
{% endif %} {% endif %}
</div> </div>
<!-- Company Stats --> <!-- Header Grid -->
<div class="grid grid-cols-1 gap-6 mb-6 md:grid-cols-3"> <div class="grid gap-2 mb-12 sm:mb-16 md:mb-8 grid-cols-1 sm:grid-cols-12 h-auto md:h-[140px]">
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800"> <!-- Company Info Card -->
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400"> <div class="flex flex-col items-center justify-center h-full col-span-1 p-2 text-center bg-white rounded-lg shadow-lg sm:col-span-3 dark:bg-gray-800">
{{ parks.count }} <h1 class="text-2xl font-bold leading-tight text-gray-900 sm:text-3xl dark:text-white">{{ company.name }}</h1>
{% if company.headquarters %}
<div class="flex items-center justify-center mt-0.5 text-sm text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i>
<p>{{ company.headquarters }}</p>
</div> </div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Theme Parks</div> {% endif %}
</div> </div>
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800"> <!-- Stats and Quick Facts -->
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400"> <div class="grid h-full grid-cols-12 col-span-1 gap-2 sm:col-span-9">
{{ parks|length }} <!-- Stats Column -->
<div class="grid grid-cols-2 col-span-12 gap-2 sm:col-span-4">
<div class="flex flex-col items-center justify-center p-2 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Total Parks</dt>
<dd class="mt-0.5 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ parks.count }}</dd>
</div>
<div class="flex flex-col items-center justify-center p-2 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Active Parks</dt>
<dd class="mt-0.5 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ parks|length }}</dd>
</div>
</div> </div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Active Parks</div>
</div> <!-- Quick Facts Grid -->
<div class="grid h-full grid-cols-3 col-span-12 gap-1 p-1.5 bg-white rounded-lg shadow-lg sm:col-span-8 dark:bg-gray-800">
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800"> <div class="flex flex-col items-center justify-center text-center p-0.5">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400"> <i class="text-sm text-blue-600 sm:text-base fas fa-ticket-alt dark:text-blue-400"></i>
{% with total_rides=0 %} <dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Total Attractions</dt>
{% for park in parks %} <dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ total_rides }}</dd>
{% with total_rides=total_rides|add:park.rides.count %}{% endwith %} </div>
{% endfor %}
{{ total_rides }} {% if company.founded_date %}
{% endwith %} <div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-calendar-alt dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Founded</dt>
<dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ company.founded_date }}</dd>
</div>
{% endif %}
{% if company.website %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-globe dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Website</dt>
<dd>
<a href="{{ company.website }}"
class="text-blue-600 text-2xs sm:text-xs hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
target="_blank" rel="noopener noreferrer">
Visit
<i class="ml-0.5 text-2xs fas fa-external-link-alt"></i>
</a>
</dd>
</div>
{% endif %}
</div> </div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Total Attractions</div>
</div> </div>
</div> </div>
{% if company.description %}
<div class="p-6 mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">About</h2>
<div class="prose dark:prose-invert max-w-none">
{{ company.description|linebreaks }}
</div>
</div>
{% endif %}
<!-- Parks List --> <!-- Parks List -->
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800"> <div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Theme Parks</h2> <h2 class="mb-6 text-xl font-semibold text-gray-900 dark:text-white">Theme Parks</h2>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for park in parks %} {% for park in parks %}
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700"> <div class="overflow-hidden transition-transform rounded-lg hover:scale-[1.02] bg-gray-50 dark:bg-gray-700">
{% if park.photos.exists %} {% if park.photos.exists %}
<img src="{{ park.photos.first.image.url }}" <img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}" alt="{{ park.name }}"
@@ -87,7 +112,7 @@
<div class="p-4"> <div class="p-4">
<h3 class="mb-2 text-lg font-semibold"> <h3 class="mb-2 text-lg font-semibold">
<a href="{% url 'parks:park_detail' park.slug %}" <a href="{% url 'parks:park_detail' park.slug %}"
class="text-blue-600 dark:text-blue-400 hover:underline"> class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
{{ park.name }} {{ park.name }}
</a> </a>
</h3> </h3>

View File

@@ -5,70 +5,100 @@
{% block content %} {% block content %}
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto">
<!-- Manufacturer Header --> <!-- Action Buttons - Above header -->
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800"> <div class="flex justify-end gap-2 mb-2">
<div class="flex flex-col items-start justify-between md:flex-row md:items-center"> {% if manufacturer.website %}
<div> <a href="{{ manufacturer.website }}" target="_blank" rel="noopener noreferrer"
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ manufacturer.name }}</h1> class="transition-transform btn-secondary hover:scale-105">
{% if manufacturer.headquarters %} <i class="mr-1 fas fa-external-link-alt"></i>Visit Website
<p class="text-gray-600 dark:text-gray-400"> </a>
<i class="mr-2 fas fa-map-marker-alt"></i>{{ manufacturer.headquarters }} {% endif %}
</p> {% if user.is_authenticated %}
{% endif %} <a href="{% url 'companies:manufacturer_edit' slug=manufacturer.slug %}"
</div> class="transition-transform btn-secondary hover:scale-105">
<div class="flex gap-2 mt-4 md:mt-0"> <i class="mr-1 fas fa-edit"></i>Edit
{% if manufacturer.website %} </a>
<a href="{{ manufacturer.website }}" target="_blank" rel="noopener noreferrer"
class="btn-secondary">
<i class="mr-2 fas fa-external-link-alt"></i>Visit Website
</a>
{% endif %}
{% if user.is_authenticated %}
<a href="{% url 'companies:manufacturer_edit' slug=manufacturer.slug %}" class="btn-secondary">
<i class="mr-2 fas fa-edit"></i>Edit
</a>
{% endif %}
</div>
</div>
{% if manufacturer.description %}
<div class="mt-6 prose dark:prose-invert max-w-none">
{{ manufacturer.description|linebreaks }}
</div>
{% endif %} {% endif %}
</div> </div>
<!-- Manufacturer Stats --> <!-- Header Grid -->
<div class="grid grid-cols-1 gap-6 mb-6 md:grid-cols-3"> <div class="grid gap-2 mb-12 sm:mb-16 md:mb-8 grid-cols-1 sm:grid-cols-12 h-auto md:h-[140px]">
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800"> <!-- Manufacturer Info Card -->
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400"> <div class="flex flex-col items-center justify-center h-full col-span-1 p-2 text-center bg-white rounded-lg shadow-lg sm:col-span-3 dark:bg-gray-800">
{{ rides.count }} <h1 class="text-2xl font-bold leading-tight text-gray-900 sm:text-3xl dark:text-white">{{ manufacturer.name }}</h1>
{% if manufacturer.headquarters %}
<div class="flex items-center justify-center mt-0.5 text-sm text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i>
<p>{{ manufacturer.headquarters }}</p>
</div> </div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Total Rides</div> {% endif %}
</div> </div>
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800"> <!-- Stats and Quick Facts -->
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400"> <div class="grid h-full grid-cols-12 col-span-1 gap-2 sm:col-span-9">
{{ rides|filter:"type='ROLLER_COASTER'"|length }} <!-- Stats Column -->
<div class="grid grid-cols-2 col-span-12 gap-2 sm:col-span-4">
<div class="flex flex-col items-center justify-center p-2 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Total Rides</dt>
<dd class="mt-0.5 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ rides.count }}</dd>
</div>
<div class="flex flex-col items-center justify-center p-2 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Coasters</dt>
<dd class="mt-0.5 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ coaster_count }}</dd>
</div>
</div> </div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Roller Coasters</div>
</div> <!-- Quick Facts Grid -->
<div class="grid h-full grid-cols-3 col-span-12 gap-1 p-1.5 bg-white rounded-lg shadow-lg sm:col-span-8 dark:bg-gray-800">
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800"> <div class="flex flex-col items-center justify-center text-center p-0.5">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400"> <i class="text-sm text-blue-600 sm:text-base fas fa-map dark:text-blue-400"></i>
{{ rides|regroup:"park"|length }} <dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Parks Served</dt>
<dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ parks_count }}</dd>
</div>
{% if manufacturer.founded_date %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-calendar-alt dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Founded</dt>
<dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ manufacturer.founded_date }}</dd>
</div>
{% endif %}
{% if manufacturer.website %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-globe dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Website</dt>
<dd>
<a href="{{ manufacturer.website }}"
class="text-blue-600 text-2xs sm:text-xs hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
target="_blank" rel="noopener noreferrer">
Visit
<i class="ml-0.5 text-2xs fas fa-external-link-alt"></i>
</a>
</dd>
</div>
{% endif %}
</div> </div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Parks with Rides</div>
</div> </div>
</div> </div>
{% if manufacturer.description %}
<div class="p-6 mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">About</h2>
<div class="prose dark:prose-invert max-w-none">
{{ manufacturer.description|linebreaks }}
</div>
</div>
{% endif %}
<!-- Rides List --> <!-- Rides List -->
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800"> <div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Rides</h2> <h2 class="mb-6 text-xl font-semibold text-gray-900 dark:text-white">Rides</h2>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for ride in rides %} {% for ride in rides %}
<div class="overflow-hidden rounded-lg bg-gray-50 dark:bg-gray-700"> <div class="overflow-hidden transition-transform rounded-lg hover:scale-[1.02] bg-gray-50 dark:bg-gray-700">
{% if ride.photos.exists %} {% if ride.photos.exists %}
<img src="{{ ride.photos.first.image.url }}" <img src="{{ ride.photos.first.image.url }}"
alt="{{ ride.name }}" alt="{{ ride.name }}"
@@ -82,7 +112,7 @@
<div class="p-4"> <div class="p-4">
<h3 class="mb-2 text-lg font-semibold"> <h3 class="mb-2 text-lg font-semibold">
<a href="{% url 'parks:rides:ride_detail' ride.park.slug ride.slug %}" <a href="{% url 'parks:rides:ride_detail' ride.park.slug ride.slug %}"
class="text-blue-600 dark:text-blue-400 hover:underline"> class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
{{ ride.name }} {{ ride.name }}
</a> </a>
</h3> </h3>

View File

@@ -0,0 +1,121 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{{ designer.name }} - ThrillWiki{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<!-- Header -->
<div class="grid grid-cols-1 gap-6 mb-8 lg:grid-cols-3">
<!-- Designer Info -->
<div class="lg:col-span-2">
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ designer.name }}</h1>
{% if designer.description %}
<div class="mt-4 prose dark:prose-invert max-w-none">
{{ designer.description|linebreaks }}
</div>
{% endif %}
</div>
</div>
<!-- Stats Card -->
<div class="lg:col-span-1">
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Quick Stats</h2>
<dl class="grid grid-cols-2 gap-4">
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Rides</dt>
<dd class="mt-1 text-3xl font-semibold text-blue-600 dark:text-blue-400">{{ stats.total_rides }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Roller Coasters</dt>
<dd class="mt-1 text-3xl font-semibold text-blue-600 dark:text-blue-400">{{ stats.total_coasters }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Parks</dt>
<dd class="mt-1 text-3xl font-semibold text-blue-600 dark:text-blue-400">{{ stats.total_parks }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Countries</dt>
<dd class="mt-1 text-3xl font-semibold text-blue-600 dark:text-blue-400">{{ stats.total_countries }}</dd>
</div>
</dl>
{% if designer.website %}
<div class="mt-6">
<a href="{{ designer.website }}" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
<i class="mr-2 fas fa-external-link-alt"></i>
Visit Website
</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Rides List -->
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-white">Designed Rides</h2>
{% if rides %}
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for ride in rides %}
<div class="p-4 transition-shadow rounded-lg bg-gray-50 hover:shadow-md dark:bg-gray-700/50">
<div class="flex items-start justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
<a href="{% url 'parks:rides:ride_detail' park_slug=ride.park.slug ride_slug=ride.slug %}"
class="hover:text-blue-600 dark:hover:text-blue-400">
{{ ride.name }}
</a>
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
at <a href="{% url 'parks:park_detail' ride.park.slug %}"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ ride.park.name }}
</a>
</p>
</div>
<span class="px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full dark:bg-blue-700 dark:text-blue-50">
{{ ride.get_category_display }}
</span>
</div>
<div class="grid grid-cols-2 gap-4 mt-4">
{% if ride.opening_date %}
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Opened</dt>
<dd class="text-gray-900 dark:text-white">{{ ride.opening_date }}</dd>
</div>
{% endif %}
{% if ride.manufacturer %}
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Manufacturer</dt>
<dd class="text-gray-900 dark:text-white">{{ ride.manufacturer.name }}</dd>
</div>
{% endif %}
{% if ride.category == 'RC' and ride.coaster_stats %}
{% if ride.coaster_stats.height_ft %}
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Height</dt>
<dd class="text-gray-900 dark:text-white">{{ ride.coaster_stats.height_ft }} ft</dd>
</div>
{% endif %}
{% if ride.coaster_stats.speed_mph %}
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Speed</dt>
<dd class="text-gray-900 dark:text-white">{{ ride.coaster_stats.speed_mph }} mph</dd>
</div>
{% endif %}
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-500 dark:text-gray-400">No rides found.</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -22,35 +22,35 @@
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto">
<!-- Action Buttons - Above header --> <!-- Action Buttons - Above header -->
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="flex justify-end gap-3 mb-4"> <div class="flex justify-end gap-2 mb-2">
<a href="{% url 'parks:park_update' park.slug %}" <a href="{% url 'parks:park_update' park.slug %}"
class="transition-transform btn-secondary hover:scale-105"> class="transition-transform btn-secondary hover:scale-105">
<i class="mr-2 fas fa-pencil-alt"></i>Edit <i class="mr-1 fas fa-pencil-alt"></i>Edit
</a> </a>
{% if perms.media.add_photo %} {% if perms.media.add_photo %}
<button class="transition-transform btn-secondary hover:scale-105" <button class="transition-transform btn-secondary hover:scale-105"
@click="$dispatch('show-photo-upload')"> @click="$dispatch('show-photo-upload')">
<i class="mr-2 fas fa-camera"></i>Upload Photo <i class="mr-1 fas fa-camera"></i>Upload Photo
</button> </button>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<!-- Header Grid --> <!-- Header Grid -->
<div class="grid h-[340px] gap-4 mb-6 grid-cols-1 sm:grid-cols-6 md:grid-cols-12"> <div class="grid gap-2 mb-12 sm:mb-16 md:mb-8 grid-cols-1 sm:grid-cols-12 h-auto md:h-[140px]">
<!-- Park Info Card --> <!-- 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"> <div class="flex flex-col items-center justify-center h-full col-span-1 p-2 text-center bg-white rounded-lg shadow-lg sm:col-span-3 dark:bg-gray-800">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1> <h1 class="text-2xl font-bold leading-tight text-gray-900 sm:text-3xl dark:text-white">{{ park.name }}</h1>
{% if park.formatted_location %} {% if park.formatted_location %}
<div class="flex items-center mt-2 text-gray-600 dark:text-gray-400"> <div class="flex items-center justify-center mt-0.5 text-sm text-gray-600 dark:text-gray-400">
<i class="mr-2 fas fa-map-marker-alt"></i> <i class="mr-1 fas fa-map-marker-alt"></i>
<p>{{ park.formatted_location }}</p> <p>{{ park.formatted_location }}</p>
</div> </div>
{% endif %} {% endif %}
<div class="flex flex-wrap items-center gap-2 mt-3"> <div class="flex flex-wrap items-center justify-center gap-1 mt-1">
<span class="status-badge text-sm font-medium {% if park.status == 'OPERATING' %}status-operating <span class="status-badge text-xs sm:text-sm font-medium py-0.5 {% 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
@@ -58,8 +58,8 @@
{{ park.get_status_display }} {{ park.get_status_display }}
</span> </span>
{% if park.average_rating %} {% if park.average_rating %}
<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="flex items-center text-xs font-medium text-yellow-800 bg-yellow-100 sm:text-sm status-badge py-0.5 dark:bg-yellow-600 dark:text-yellow-50">
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span> <span class="mr-0.5 text-yellow-500 dark:text-yellow-200"></span>
{{ park.average_rating|floatformat:1 }}/10 {{ park.average_rating|floatformat:1 }}/10
</span> </span>
{% endif %} {% endif %}
@@ -67,36 +67,36 @@
</div> </div>
<!-- Stats and Quick Facts --> <!-- 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"> <div class="grid h-full grid-cols-12 col-span-1 gap-2 sm:col-span-9">
<!-- Stats Column --> <!-- Stats Column -->
<div class="flex flex-col col-span-1 gap-4 sm:col-span-2 md:col-span-4"> <div class="grid-cols-2 col-span-12 gap-2 text-sky-400grid sm:grid-cols-1 md:grid-cols-2 sm:col-span-4">
<!-- Total Rides Card --> <!-- Total Rides Card -->
{% if park.total_rides %} {% if park.total_rides %}
<a href="{% url 'parks:rides:ride_list' park.slug %}" <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"> class="flex flex-col items-center justify-center p-2 text-center 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> <dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Total Rides</dt>
<dd class="mt-2 text-4xl font-bold text-gray-900 dark:text-white">{{ park.total_rides }}</dd> <dd class="mt-0.5 text-xl font-bold text-sky-900 hover:text-sky-800 sm:text-2xl dark:text-sky-400 dark:hover:text-sky-300">{{ park.total_rides }}</dd>
</a> </a>
{% endif %} {% endif %}
<!-- Total Roller Coasters Card --> <!-- Total Roller Coasters Card -->
{% if park.total_roller_coasters %} {% if park.total_roller_coasters %}
<div class="flex flex-col flex-1 p-4 bg-white rounded-lg shadow-lg dark:bg-gray-800"> <div class="flex flex-col items-center justify-center p-2 text-center 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> <dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Roller Coasters</dt>
<dd class="mt-2 text-4xl font-bold text-gray-900 dark:text-white">{{ park.total_roller_coasters }}</dd> <dd class="mt-0.5 text-xl font-bold text-sky-900 hover:text-sky-800 sm:text-2xl dark:text-sky-400 dark:hover:text-sky-300">{{ park.total_roller_coasters }}</dd>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Quick Facts Grid --> <!-- 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"> <div class="grid h-full grid-cols-3 col-span-12 gap-1 p-1.5 bg-white rounded-lg shadow-lg sm:col-span-8 dark:bg-gray-800">
{% if park.owner %} {% if park.owner %}
<div class="flex flex-col items-center justify-center text-center lg:col-span-2"> <div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="mb-1 text-lg text-blue-600 fas fa-building dark:text-blue-400"></i> <i class="text-sm text-blue-600 sm:text-base fas fa-building dark:text-blue-400"></i>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Owner/Operator</dt> <dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Owner</dt>
<dd class="mt-0.5"> <dd>
<a href="{% url 'companies:company_detail' park.owner.slug %}" <a href="{% url 'companies:company_detail' park.owner.slug %}"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"> class="text-blue-600 text-2xs sm:text-xs hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ park.owner.name }} {{ park.owner.name }}
</a> </a>
</dd> </dd>
@@ -104,39 +104,23 @@
{% endif %} {% endif %}
{% if park.opening_date %} {% if park.opening_date %}
<div class="flex flex-col items-center justify-center text-center"> <div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="mb-1 text-lg text-blue-600 fas fa-calendar-alt dark:text-blue-400"></i> <i class="text-sm text-blue-600 sm:text-base fas fa-calendar-alt dark:text-blue-400"></i>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Opening Date</dt> <dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Opened</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.opening_date }}</dd> <dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ park.opening_date }}</dd>
</div>
{% endif %}
{% if park.operating_season %}
<div class="flex flex-col items-center justify-center text-center">
<i class="mb-1 text-lg text-blue-600 fas fa-clock dark:text-blue-400"></i>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Operating Season</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.operating_season }}</dd>
</div>
{% endif %}
{% if park.size_acres %}
<div class="flex flex-col items-center justify-center text-center">
<i class="mb-1 text-lg text-blue-600 fas fa-ruler-combined dark:text-blue-400"></i>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Size</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.size_acres }} acres</dd>
</div> </div>
{% endif %} {% endif %}
{% if park.website %} {% if park.website %}
<div class="flex flex-col items-center justify-center text-center lg:col-span-2"> <div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="mb-1 text-lg text-blue-600 fas fa-globe dark:text-blue-400"></i> <i class="text-sm text-blue-600 sm:text-base fas fa-globe dark:text-blue-400"></i>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Website</dt> <dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Website</dt>
<dd class="mt-0.5"> <dd>
<a href="{{ park.website }}" <a href="{{ park.website }}"
class="inline-flex items-center text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" class="inline-flex items-center text-blue-600 text-2xs sm:text-xs hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
target="_blank" rel="noopener noreferrer"> target="_blank" rel="noopener noreferrer">
Official Website Visit
<i class="ml-1 fas fa-external-link-alt"></i> <i class="ml-0.5 text-2xs fas fa-external-link-alt"></i>
</a> </a>
</dd> </dd>
</div> </div>
@@ -144,7 +128,8 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Photos -->
<!-- Photos Section -->
{% 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">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2> <h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2>

View File

@@ -0,0 +1,154 @@
{% extends "base/base.html" %}
{% load static %}
{% load ride_tags %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container px-4 mx-auto">
<div class="flex flex-col items-start justify-between gap-4 mb-6 md:flex-row md:items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ title }}</h1>
{% if park %}
<a href="{% url 'parks:park_detail' park.slug %}" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
Back to {{ park.name }}
</a>
{% endif %}
</div>
</div>
<!-- Category Filters -->
<div class="flex flex-wrap gap-4 mb-8">
{% if park %}
<a href="{% url 'parks:park_roller_coasters' park.slug %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'RC' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Roller Coasters
</a>
<a href="{% url 'parks:park_dark_rides' park.slug %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'DR' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Dark Rides
</a>
<a href="{% url 'parks:park_flat_rides' park.slug %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'FR' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Flat Rides
</a>
<a href="{% url 'parks:park_water_rides' park.slug %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'WR' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Water Rides
</a>
<a href="{% url 'parks:park_transports' park.slug %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'TR' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Transports
</a>
<a href="{% url 'parks:park_others' park.slug %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'OT' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Others
</a>
{% else %}
<a href="{% url 'rides:roller_coasters' %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'RC' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Roller Coasters
</a>
<a href="{% url 'rides:dark_rides' %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'DR' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Dark Rides
</a>
<a href="{% url 'rides:flat_rides' %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'FR' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Flat Rides
</a>
<a href="{% url 'rides:water_rides' %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'WR' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Water Rides
</a>
<a href="{% url 'rides:transports' %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'TR' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Transports
</a>
<a href="{% url 'rides:others' %}"
class="px-4 py-2 rounded-lg transition-colors {% if category_code == 'OT' %}bg-blue-600 text-white dark:bg-blue-500{% else %}bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600{% endif %}">
Others
</a>
{% endif %}
</div>
{% if not categories %}
<p class="text-gray-600 dark:text-gray-400">No rides found.</p>
{% endif %}
{% for category_name, rides in categories.items %}
<div class="mb-10">
<h2 class="mb-4 text-2xl font-semibold text-gray-900 dark:text-white">{{ category_name }}s</h2>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for ride in rides %}
<div class="overflow-hidden transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
<div class="aspect-w-16 aspect-h-9">
{% if ride.photos.exists %}
<img src="{{ ride.photos.first.image.url }}"
alt="{{ ride.name }}"
class="object-cover w-full">
{% else %}
<img src="{% get_ride_placeholder_image ride.category %}"
alt="{{ ride.name }}"
class="object-cover w-full">
{% endif %}
</div>
<div class="p-4">
<h3 class="mb-2 text-xl font-bold">
<a href="{% url 'parks:rides:ride_detail' ride.park.slug ride.slug %}"
class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
{{ ride.name }}
</a>
</h3>
{% if not park %}
<p class="mb-3 text-gray-600 dark:text-gray-400">
at <a href="{% url 'parks:park_detail' ride.park.slug %}"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ ride.park.name }}
</a>
</p>
{% endif %}
{% if ride.manufacturer %}
<p class="mb-3 text-gray-600 dark:text-gray-400">{{ ride.manufacturer.name }}</p>
{% endif %}
<div class="flex flex-wrap gap-2">
<span class="text-blue-800 bg-blue-100 status-badge dark:bg-blue-400/30 dark:text-blue-200 dark:ring-1 dark:ring-blue-400/30">
{{ ride.get_category_display }}
</span>
<span class="status-badge {% if ride.status == 'OPERATING' %}status-operating
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif ride.status == 'DEMOLISHED' %}status-demolished
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ ride.get_status_display }}
</span>
{% if ride.average_rating %}
<span class="text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-400/30 dark:text-yellow-200 dark:ring-1 dark:ring-yellow-400/30">
<i class="mr-1 text-yellow-500 fas fa-star dark:text-yellow-300"></i>
{{ ride.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</div>
{% if ride.coaster_stats %}
<div class="grid grid-cols-2 gap-2 mt-4">
{% if ride.coaster_stats.height_ft %}
<div class="text-sm text-gray-600 dark:text-gray-400">
Height: {{ ride.coaster_stats.height_ft }}ft
</div>
{% endif %}
{% if ride.coaster_stats.speed_mph %}
<div class="text-sm text-gray-600 dark:text-gray-400">
Speed: {{ ride.coaster_stats.speed_mph }}mph
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -5,54 +5,161 @@
{% block content %} {% block content %}
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto">
<!-- Header --> <!-- 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 items-start justify-between"> <div class="flex justify-end gap-2 mb-2">
<div> <a href="{% url 'parks:rides:ride_edit' park_slug=ride.park.slug ride_slug=ride.slug %}"
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ ride.name }}</h1> class="transition-transform btn-secondary hover:scale-105">
<p class="mb-2 text-gray-600 dark:text-gray-400"> <i class="mr-1 fas fa-pencil-alt"></i>Edit
at <a href="{% url 'parks:park_detail' ride.park.slug %}" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"> </a>
{{ ride.park.name }} {% if perms.media.add_photo %}
</a> <button class="transition-transform btn-secondary hover:scale-105"
{% if ride.park_area %} @click="$dispatch('show-photo-upload')">
- {{ ride.park_area.name }} <i class="mr-1 fas fa-camera"></i>Upload Photo
{% endif %} </button>
</p> {% endif %}
<div class="flex flex-wrap gap-2 mt-3"> </div>
<span class="status-badge {% if ride.status == 'OPERATING' %}status-operating {% endif %}
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction <!-- Header Grid -->
{% elif ride.status == 'DEMOLISHED' %}status-demolished <div class="grid grid-cols-1 gap-2 mb-8 sm:grid-cols-12">
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}"> <!-- Ride Info Card -->
{{ ride.get_status_display }} <div class="flex flex-col items-center justify-center h-full col-span-1 p-2 text-center bg-white rounded-lg shadow-lg sm:col-span-3 dark:bg-gray-800">
</span> <h1 class="text-2xl font-bold leading-tight text-gray-900 sm:text-3xl dark:text-white">{{ ride.name }}</h1>
<span class="text-blue-800 bg-blue-100 status-badge dark:bg-blue-700 dark:text-blue-50">
{{ ride.get_category_display }} <div class="flex items-center justify-center mt-0.5 text-sm text-gray-600 dark:text-gray-400">
</span> at <a href="{% url 'parks:park_detail' ride.park.slug %}" class="ml-1 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300">
{% if ride.average_rating %} {{ ride.park.name }}
<span class="flex items-center text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50"> </a>
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span> {% if ride.park_area %}
{{ ride.average_rating|floatformat:1 }}/10 - {{ ride.park_area.name }}
</span> {% endif %}
{% endif %}
</div>
</div> </div>
{% if user.is_authenticated %}
<div class="flex gap-2"> <div class="flex flex-wrap items-center justify-center gap-1 mt-1">
<a href="{% url 'parks:rides:ride_edit' park_slug=ride.park.slug ride_slug=ride.slug %}" class="btn-secondary"> <span class="status-badge text-xs sm:text-sm font-medium py-0.5 {% if ride.status == 'OPERATING' %}status-operating
<i class="mr-2 fas fa-pencil-alt"></i>Edit {% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
</a> {% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction
{% if perms.media.add_photo %} {% elif ride.status == 'DEMOLISHED' %}status-demolished
<button class="btn-secondary" @click="$dispatch('show-photo-upload')"> {% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
<i class="mr-2 fas fa-camera"></i>Upload Photo {{ ride.get_status_display }}
</button> </span>
<span class="text-blue-800 bg-blue-100 status-badge text-xs sm:text-sm font-medium py-0.5 dark:bg-blue-700 dark:text-blue-50">
{{ ride.get_category_display }}
</span>
{% if ride.average_rating %}
<span class="flex items-center text-xs font-medium text-yellow-800 bg-yellow-100 sm:text-sm status-badge py-0.5 dark:bg-yellow-600 dark:text-yellow-50">
<span class="mr-0.5 text-yellow-500 dark:text-yellow-200"></span>
{{ ride.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</div>
</div>
<!-- Stats and Quick Facts -->
<div class="grid h-full grid-cols-12 col-span-1 gap-2 sm:col-span-9">
<!-- Stats Column -->
<div class="grid grid-cols-2 col-span-12 gap-2 sm:col-span-4">
{% if coaster_stats %}
{% if coaster_stats.height_ft %}
<div class="flex flex-col items-center justify-center p-2 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Height</dt>
<dd class="mt-0.5 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ coaster_stats.height_ft }} ft</dd>
</div>
{% endif %} {% endif %}
{% if coaster_stats.speed_mph %}
<div class="flex flex-col items-center justify-center p-2 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Speed</dt>
<dd class="mt-0.5 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ coaster_stats.speed_mph }} mph</dd>
</div>
{% endif %}
{% if coaster_stats.inversions %}
<div class="flex flex-col items-center justify-center p-2 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Inversions</dt>
<dd class="mt-0.5 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ coaster_stats.inversions }}</dd>
</div>
{% endif %}
{% if coaster_stats.length_ft %}
<div class="flex flex-col items-center justify-center p-2 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Length</dt>
<dd class="mt-0.5 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ coaster_stats.length_ft }} ft</dd>
</div>
{% endif %}
{% endif %}
</div>
<!-- Quick Facts Grid -->
<div class="grid h-full grid-cols-3 col-span-12 gap-1 p-1.5 bg-white rounded-lg shadow-lg sm:col-span-8 dark:bg-gray-800">
{% if ride.manufacturer %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-industry dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Manufacturer</dt>
<dd>
<a href="{% url 'companies:manufacturer_detail' ride.manufacturer.slug %}"
class="text-blue-600 text-2xs sm:text-xs hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ ride.manufacturer.name }}
</a>
</dd>
</div> </div>
{% endif %} {% endif %}
{% if ride.designer %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-drafting-compass dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Designer</dt>
<dd>
<a href="{% url 'designers:designer_detail' ride.designer.slug %}"
class="text-blue-600 text-2xs sm:text-xs hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ ride.designer.name }}
</a>
</dd>
</div>
{% endif %}
{% if coaster_stats.roller_coaster_type %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-train dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Coaster Type</dt>
<dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ coaster_stats.get_roller_coaster_type_display }}</dd>
</div>
{% endif %}
{% if coaster_stats.track_material %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-layer-group dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Track Material</dt>
<dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ coaster_stats.get_track_material_display }}</dd>
</div>
{% endif %}
{% if ride.opening_date %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-calendar-alt dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Opened</dt>
<dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ ride.opening_date }}</dd>
</div>
{% endif %}
{% if ride.capacity_per_hour %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-users dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Capacity</dt>
<dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ ride.capacity_per_hour }}/hr</dd>
</div>
{% endif %}
{% if coaster_stats.launch_type %}
<div class="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-rocket dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Launch Type</dt>
<dd class="text-gray-900 text-2xs sm:text-xs dark:text-white">{{ coaster_stats.get_launch_type_display }}</dd>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
<!-- Photos --> <!-- Photos Section -->
{% if ride.photos.exists %} {% if ride.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">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2> <h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2>
@@ -60,13 +167,50 @@
</div> </div>
{% endif %} {% endif %}
<!-- Reviews Section -->
<div class="p-6 mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Reviews</h2>
{% if user.is_authenticated %}
<button class="btn-primary">
<i class="mr-2 fas fa-star"></i>
Write a Review
</button>
{% endif %}
</div>
{% if ride.reviews.exists %}
<div class="space-y-4">
{% for review in ride.reviews.all %}
<div class="pb-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-start justify-between">
<div>
<h3 class="font-semibold text-gray-900 dark:text-white">{{ review.title }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
by {{ review.user.username }} on {{ review.created_at|date }}
</p>
</div>
<div class="flex items-center">
<span class="mr-1 text-yellow-400"></span>
<span class="text-gray-900 dark:text-white">{{ review.rating }}/10</span>
</div>
</div>
<p class="mt-2 text-gray-700 dark:text-gray-300">{{ review.content }}</p>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-500 dark:text-gray-400">No reviews yet. Be the first to review this ride!</p>
{% endif %}
</div>
<!-- Main Content Grid --> <!-- Main Content Grid -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Column - Description and Details --> <!-- Left Column - Description and Details -->
<div class="lg:col-span-2"> <div class="lg:col-span-2">
{% if ride.description %} {% if ride.description %}
<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">About</h2> <h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Trivia</h2>
<div class="prose dark:prose-invert max-w-none"> <div class="prose dark:prose-invert max-w-none">
{{ ride.description|linebreaks }} {{ ride.description|linebreaks }}
</div> </div>
@@ -79,8 +223,8 @@
<div class="space-y-2"> <div class="space-y-2">
{% for name_history in ride.previous_names %} {% for name_history in ride.previous_names %}
<div class="flex justify-between"> <div class="flex justify-between">
<span>{{ name_history.name }}</span> <span class="text-gray-900 dark:text-white">{{ name_history.name }}</span>
<span class="text-gray-500">{{ name_history.period }}</span> <span class="text-gray-500 dark:text-gray-400">{{ name_history.period }}</span>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -91,44 +235,129 @@
<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">Roller Coaster Statistics</h2> <h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Roller Coaster Statistics</h2>
<div class="grid grid-cols-2 gap-4 md:grid-cols-3"> <div class="grid grid-cols-2 gap-4 md:grid-cols-3">
<!-- Coaster Type -->
{% if coaster_stats.roller_coaster_type %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Coaster Type</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.get_roller_coaster_type_display }}
</span>
</div>
{% endif %}
<!-- Height Stats -->
{% if coaster_stats.height_ft %} {% if coaster_stats.height_ft %}
<div> <div>
<span class="block text-gray-500">Height</span> <span class="block text-gray-500 dark:text-gray-400">Maximum Height</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white"> <span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.height_ft }} ft {{ coaster_stats.height_ft }} ft
</span> </span>
</div> </div>
{% endif %} {% endif %}
{% if coaster_stats.max_drop_height_ft %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Drop Height</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.max_drop_height_ft }} ft
</span>
</div>
{% endif %}
<!-- Track Stats -->
{% if coaster_stats.length_ft %} {% if coaster_stats.length_ft %}
<div> <div>
<span class="block text-gray-500">Length</span> <span class="block text-gray-500 dark:text-gray-400">Track Length</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white"> <span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.length_ft }} ft {{ coaster_stats.length_ft }} ft
</span> </span>
</div> </div>
{% endif %} {% endif %}
{% if coaster_stats.track_type %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Track Layout</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.track_type }}
</span>
</div>
{% endif %}
{% if coaster_stats.track_material %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Track Material</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.get_track_material_display }}
</span>
</div>
{% endif %}
<!-- Speed and Time -->
{% if coaster_stats.speed_mph %} {% if coaster_stats.speed_mph %}
<div> <div>
<span class="block text-gray-500">Speed</span> <span class="block text-gray-500 dark:text-gray-400">Maximum Speed</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white"> <span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.speed_mph }} mph {{ coaster_stats.speed_mph }} mph
</span> </span>
</div> </div>
{% endif %} {% endif %}
<div>
<span class="block text-gray-500">Inversions</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.inversions }}
</span>
</div>
{% if coaster_stats.ride_time_seconds %} {% if coaster_stats.ride_time_seconds %}
<div> <div>
<span class="block text-gray-500">Ride Duration</span> <span class="block text-gray-500 dark:text-gray-400">Ride Duration</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white"> <span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.ride_time_seconds }} sec {{ coaster_stats.ride_time_seconds }} sec
</span> </span>
</div> </div>
{% endif %} {% endif %}
<!-- Train Details -->
{% if coaster_stats.train_style %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Train Style</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.train_style }}
</span>
</div>
{% endif %}
{% if coaster_stats.trains_count %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Number of Trains</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.trains_count }}
</span>
</div>
{% endif %}
{% if coaster_stats.cars_per_train %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Cars per Train</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.cars_per_train }}
</span>
</div>
{% endif %}
{% if coaster_stats.seats_per_car %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Seats per Car</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.seats_per_car }}
</span>
</div>
{% endif %}
<!-- Other Stats -->
{% if coaster_stats.inversions %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Inversions</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.inversions }}
</span>
</div>
{% endif %}
{% if coaster_stats.launch_type %}
<div>
<span class="block text-gray-500 dark:text-gray-400">Launch Type</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{{ coaster_stats.get_launch_type_display }}
</span>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -140,18 +369,29 @@
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Quick Facts</h2> <h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Quick Facts</h2>
<dl class="space-y-4"> <dl class="space-y-4">
<div> <div>
<dt class="text-gray-500">Manufacturer</dt> <dt class="text-gray-500 dark:text-gray-400">Manufacturer</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ ride.manufacturer }}</dd> <dd class="font-medium text-gray-900 dark:text-white">{{ ride.manufacturer }}</dd>
</div> </div>
{% if ride.designer %}
<div>
<dt class="text-gray-500 dark:text-gray-400">Designer</dt>
<dd class="font-medium text-gray-900 dark:text-white">
<a href="{% url 'designers:designer_detail' ride.designer.slug %}"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ ride.designer.name }}
</a>
</dd>
</div>
{% endif %}
{% if ride.model_name %} {% if ride.model_name %}
<div> <div>
<dt class="text-gray-500">Model</dt> <dt class="text-gray-500 dark:text-gray-400">Model</dt>
<dd class="font-medium text-gray-900 dark:text-white">{{ ride.model_name }}</dd> <dd class="font-medium text-gray-900 dark:text-white">{{ ride.model_name }}</dd>
</div> </div>
{% endif %} {% endif %}
{% if ride.opening_date %} {% if ride.opening_date %}
<div> <div>
<dt class="text-gray-500">Opening Date</dt> <dt class="text-gray-500 dark:text-gray-400">Opening Date</dt>
<dd class="font-medium text-gray-900 dark:text-white"> <dd class="font-medium text-gray-900 dark:text-white">
{{ ride.opening_date }} {{ ride.opening_date }}
</dd> </dd>
@@ -159,7 +399,7 @@
{% endif %} {% endif %}
{% if ride.status_since %} {% if ride.status_since %}
<div> <div>
<dt class="text-gray-500">Status Since</dt> <dt class="text-gray-500 dark:text-gray-400">Status Since</dt>
<dd class="font-medium text-gray-900 dark:text-white"> <dd class="font-medium text-gray-900 dark:text-white">
{{ ride.status_since }} {{ ride.status_since }}
</dd> </dd>
@@ -167,7 +407,7 @@
{% endif %} {% endif %}
{% if ride.closing_date %} {% if ride.closing_date %}
<div> <div>
<dt class="text-gray-500">Closing Date</dt> <dt class="text-gray-500 dark:text-gray-400">Closing Date</dt>
<dd class="font-medium text-gray-900 dark:text-white"> <dd class="font-medium text-gray-900 dark:text-white">
{{ ride.closing_date }} {{ ride.closing_date }}
</dd> </dd>
@@ -175,7 +415,7 @@
{% endif %} {% endif %}
{% if ride.capacity_per_hour %} {% if ride.capacity_per_hour %}
<div> <div>
<dt class="text-gray-500">Capacity</dt> <dt class="text-gray-500 dark:text-gray-400">Capacity</dt>
<dd class="font-medium text-gray-900 dark:text-white"> <dd class="font-medium text-gray-900 dark:text-white">
{{ ride.capacity_per_hour }} riders/hour {{ ride.capacity_per_hour }} riders/hour
</dd> </dd>
@@ -183,7 +423,7 @@
{% endif %} {% endif %}
{% if ride.min_height_in %} {% if ride.min_height_in %}
<div> <div>
<dt class="text-gray-500">Minimum Height</dt> <dt class="text-gray-500 dark:text-gray-400">Minimum Height</dt>
<dd class="font-medium text-gray-900 dark:text-white"> <dd class="font-medium text-gray-900 dark:text-white">
{{ ride.min_height_in }} inches {{ ride.min_height_in }} inches
</dd> </dd>
@@ -206,65 +446,33 @@
</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>
{% empty %} {% empty %}
<p class="text-gray-500">No history available.</p> <p class="text-gray-500 dark:text-gray-400">No history available.</p>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Reviews Section -->
<div class="p-6 mt-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Reviews</h2>
{% if user.is_authenticated %}
<button class="btn-primary">
<i class="mr-2 fas fa-star"></i>
Write a Review
</button>
{% endif %}
</div>
{% if ride.reviews.exists %}
<div class="space-y-4">
{% for review in ride.reviews.all %}
<div class="pb-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-start justify-between">
<div>
<h3 class="font-semibold text-gray-900 dark:text-white">{{ review.title }}</h3>
<p class="text-sm text-gray-500">
by {{ review.user.username }} on {{ review.created_at|date }}
</p>
</div>
<div class="flex items-center">
<span class="mr-1 text-yellow-400"></span>
<span>{{ review.rating }}/10</span>
</div>
</div>
<p class="mt-2 text-gray-700 dark:text-gray-300">{{ review.content }}</p>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-500">No reviews yet. Be the first to review this ride!</p>
{% endif %}
</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
x-data="{ show: false }"
@show-photo-upload.window="show = true" @show-photo-upload.window="show = true"
x-show="show" x-show="show"
class="fixed inset-0 z-50 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">
<div class="w-full max-w-2xl p-6 mx-4 bg-white rounded-lg shadow-xl dark:bg-gray-800"> <div class="w-full max-w-2xl p-6 mx-4 bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">

View File

@@ -45,6 +45,7 @@ INSTALLED_APPS = [
"media.apps.MediaConfig", "media.apps.MediaConfig",
"moderation", "moderation",
"history_tracking", "history_tracking",
"designers",
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@@ -18,6 +18,7 @@ urlpatterns = [
# Other URLs # Other URLs
path("reviews/", include("reviews.urls")), path("reviews/", include("reviews.urls")),
path("companies/", include("companies.urls")), path("companies/", include("companies.urls")),
path("designers/", include("designers.urls", namespace="designers")),
path("photos/", include("media.urls", namespace="photos")), # Add photos URLs path("photos/", include("media.urls", namespace="photos")), # Add photos URLs
path("search/", SearchView.as_view(), name="search"), path("search/", SearchView.as_view(), name="search"),
path( path(