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 9de9311670
commit 209e6add1c
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 import messages
from django.http import HttpResponseRedirect
from django.db.models import Count, Sum
from .models import Company, Manufacturer
from .forms import CompanyForm, ManufacturerForm
from rides.models import Ride
@@ -173,9 +174,13 @@ class CompanyDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionM
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['parks'] = Park.objects.filter(
parks = Park.objects.filter(
owner=self.object
).select_related('owner')
context['parks'] = parks
context['total_rides'] = Ride.objects.filter(park__in=parks).count()
return context
def get_redirect_url_pattern(self):
@@ -195,9 +200,14 @@ class ManufacturerDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmis
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['rides'] = Ride.objects.filter(
rides = Ride.objects.filter(
manufacturer=self.object
).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
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 . import views
from rides.views import ParkSingleCategoryListView
app_name = "parks"
@@ -17,6 +18,14 @@ urlpatterns = [
# Area views
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
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 Meta:
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',
'accessibility_options', 'capacity_per_hour', 'ride_duration_seconds', 'description']
widgets = {
@@ -20,6 +20,9 @@ class RideForm(forms.ModelForm):
'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'
}),
'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={
'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'
)
manufacturer = models.ForeignKey(
'companies.manufacturer', on_delete=models.CASCADE, null=False, blank=False
'companies.manufacturer',
on_delete=models.CASCADE,
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'
)
# other fields...
model_name = models.CharField(max_length=255, blank=True)
status = models.CharField(
max_length=20,
@@ -108,6 +117,32 @@ class RollerCoasterStats(models.Model):
('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,
on_delete=models.CASCADE,
@@ -134,6 +169,26 @@ class RollerCoasterStats(models.Model):
inversions = models.PositiveIntegerField(default=0)
ride_time_seconds = models.PositiveIntegerField(null=True, 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(
max_length=20,
choices=LAUNCH_CHOICES,

View File

@@ -4,8 +4,17 @@ from . import views
app_name = 'rides' # Add namespace
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('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('<slug:ride_slug>/edit/', views.RideUpdateView.as_view(), name='ride_edit'),
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.contenttypes.models import ContentType
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 .forms import RideForm
from parks.models import Park
from core.views import SlugRedirectMixin
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin
from moderation.models import EditSubmission
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):
model = Ride
form_class = RideForm

View File

@@ -2253,6 +2253,10 @@ select {
grid-column: span 1 / span 1;
}
.col-span-12 {
grid-column: span 12 / span 12;
}
.mx-1 {
margin-left: 0.25rem;
margin-right: 0.25rem;
@@ -2361,6 +2365,30 @@ select {
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 {
display: block;
}
@@ -2433,6 +2461,10 @@ select {
height: 340px;
}
.h-auto {
height: auto;
}
.max-h-60 {
max-height: 15rem;
}
@@ -2441,6 +2473,10 @@ select {
max-height: 90vh;
}
.max-h-\[340px\] {
max-height: 340px;
}
.min-h-\[calc\(100vh-16rem\)\] {
min-height: calc(100vh - 16rem);
}
@@ -2453,6 +2489,10 @@ select {
min-height: 0px;
}
.min-h-\[200px\] {
min-height: 200px;
}
.w-16 {
width: 4rem;
}
@@ -2526,6 +2566,10 @@ select {
flex: 1 1 0%;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
@@ -2659,6 +2703,14 @@ select {
gap: 2rem;
}
.gap-1\.5 {
gap: 0.375rem;
}
.gap-1 {
gap: 0.25rem;
}
.gap-x-8 {
-moz-column-gap: 2rem;
column-gap: 2rem;
@@ -2732,6 +2784,10 @@ select {
overflow: hidden;
}
.overflow-y-auto {
overflow-y: auto;
}
.rounded {
border-radius: 0.25rem;
}
@@ -2935,6 +2991,11 @@ select {
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 {
--tw-bg-opacity: 0.5;
}
@@ -3006,6 +3067,18 @@ select {
padding: 2rem;
}
.p-2\.5 {
padding: 0.625rem;
}
.p-0\.5 {
padding: 0.125rem;
}
.p-1\.5 {
padding: 0.375rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@@ -3071,6 +3144,11 @@ select {
padding-bottom: 2rem;
}
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.pb-4 {
padding-bottom: 1rem;
}
@@ -3079,6 +3157,10 @@ select {
text-align: center;
}
.align-middle {
vertical-align: middle;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
@@ -3114,6 +3196,11 @@ select {
line-height: 1rem;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.font-bold {
font-weight: 700;
}
@@ -3126,6 +3213,10 @@ select {
font-weight: 600;
}
.leading-tight {
line-height: 1.25;
}
.text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
@@ -3240,6 +3331,16 @@ select {
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;
}
@@ -3335,6 +3436,12 @@ select {
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 {
transition-duration: 100ms;
}
@@ -3455,6 +3562,11 @@ select {
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 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
@@ -3494,10 +3606,41 @@ select {
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 {
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 {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
@@ -3765,6 +3908,11 @@ select {
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 *) {
--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);
@@ -3838,6 +3986,26 @@ select {
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) {
.sm\:col-span-2 {
grid-column: span 2 / span 2;
@@ -3847,6 +4015,54 @@ select {
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 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -3855,6 +4071,26 @@ select {
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]) {
--tw-space-x-reverse: 0;
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-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) {
@@ -3893,6 +4164,14 @@ select {
margin-top: 0px;
}
.md\:mb-8 {
margin-bottom: 2rem;
}
.md\:h-\[140px\] {
height: 140px;
}
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}

View File

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

View File

@@ -5,70 +5,100 @@
{% block content %}
<div class="container px-4 mx-auto">
<!-- Manufacturer Header -->
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex flex-col items-start justify-between md:flex-row md:items-center">
<div>
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ manufacturer.name }}</h1>
{% if manufacturer.headquarters %}
<p class="text-gray-600 dark:text-gray-400">
<i class="mr-2 fas fa-map-marker-alt"></i>{{ manufacturer.headquarters }}
</p>
{% endif %}
</div>
<div class="flex gap-2 mt-4 md:mt-0">
<!-- Action Buttons - Above header -->
<div class="flex justify-end gap-2 mb-2">
{% if manufacturer.website %}
<a href="{{ manufacturer.website }}" target="_blank" rel="noopener noreferrer"
class="btn-secondary">
<i class="mr-2 fas fa-external-link-alt"></i>Visit Website
class="transition-transform btn-secondary hover:scale-105">
<i class="mr-1 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 href="{% url 'companies:manufacturer_edit' slug=manufacturer.slug %}"
class="transition-transform btn-secondary hover:scale-105">
<i class="mr-1 fas fa-edit"></i>Edit
</a>
{% endif %}
</div>
<!-- Header Grid -->
<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]">
<!-- Manufacturer Info Card -->
<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-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>
{% endif %}
</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">
<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>
<!-- 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="flex flex-col items-center justify-center text-center p-0.5">
<i class="text-sm text-blue-600 sm:text-base fas fa-map dark:text-blue-400"></i>
<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>
{% if manufacturer.description %}
<div class="mt-6 prose dark:prose-invert max-w-none">
<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 %}
</div>
<!-- Manufacturer Stats -->
<div class="grid grid-cols-1 gap-6 mb-6 md:grid-cols-3">
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{{ rides.count }}
</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Total Rides</div>
</div>
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{{ rides|filter:"type='ROLLER_COASTER'"|length }}
</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Roller Coasters</div>
</div>
<div class="p-6 text-center bg-white rounded-lg shadow dark:bg-gray-800">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
{{ rides|regroup:"park"|length }}
</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">Parks with Rides</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-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">
{% 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 %}
<img src="{{ ride.photos.first.image.url }}"
alt="{{ ride.name }}"
@@ -82,7 +112,7 @@
<div class="p-4">
<h3 class="mb-2 text-lg font-semibold">
<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 }}
</a>
</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">
<!-- Action Buttons - Above header -->
{% 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 %}"
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>
{% if perms.media.add_photo %}
<button class="transition-transform btn-secondary hover:scale-105"
@click="$dispatch('show-photo-upload')">
<i class="mr-2 fas fa-camera"></i>Upload Photo
<i class="mr-1 fas fa-camera"></i>Upload Photo
</button>
{% endif %}
</div>
{% endif %}
<!-- Header Grid -->
<div class="grid h-[340px] gap-4 mb-6 grid-cols-1 sm:grid-cols-6 md:grid-cols-12">
<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 -->
<div class="flex flex-col h-full col-span-1 p-4 overflow-auto bg-white rounded-lg shadow-lg sm:col-span-2 md:col-span-3 dark:bg-gray-800">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ park.name }}</h1>
<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-2xl font-bold leading-tight text-gray-900 sm:text-3xl dark:text-white">{{ park.name }}</h1>
{% if park.formatted_location %}
<div class="flex items-center mt-2 text-gray-600 dark:text-gray-400">
<i class="mr-2 fas fa-map-marker-alt"></i>
<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>{{ park.formatted_location }}</p>
</div>
{% endif %}
<div class="flex flex-wrap items-center gap-2 mt-3">
<span class="status-badge text-sm font-medium {% if park.status == 'OPERATING' %}status-operating
<div class="flex flex-wrap items-center justify-center gap-1 mt-1">
<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 == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
@@ -58,8 +58,8 @@
{{ park.get_status_display }}
</span>
{% 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="mr-1 text-yellow-500 dark:text-yellow-200"></span>
<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>
{{ park.average_rating|floatformat:1 }}/10
</span>
{% endif %}
@@ -67,36 +67,36 @@
</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">
<div class="grid h-full grid-cols-12 col-span-1 gap-2 sm:col-span-9">
<!-- 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 -->
{% 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>
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-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 hover:text-sky-800 sm:text-2xl dark:text-sky-400 dark:hover:text-sky-300">{{ 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 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">Roller Coasters</dt>
<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>
{% endif %}
</div>
<!-- Quick Facts Grid -->
<div class="grid h-full grid-cols-2 col-span-1 gap-4 p-4 bg-white rounded-lg shadow-lg sm:grid-cols-2 sm:col-span-4 md:grid-cols-4 md:col-span-8 lg:grid-cols-6 dark:bg-gray-800">
<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 %}
<div class="flex flex-col items-center justify-center text-center lg:col-span-2">
<i class="mb-1 text-lg text-blue-600 fas fa-building dark:text-blue-400"></i>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Owner/Operator</dt>
<dd class="mt-0.5">
<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-building dark:text-blue-400"></i>
<dt class="font-medium text-gray-500 text-2xs dark:text-gray-400">Owner</dt>
<dd>
<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 }}
</a>
</dd>
@@ -104,39 +104,23 @@
{% endif %}
{% if park.opening_date %}
<div class="flex flex-col items-center justify-center text-center">
<i class="mb-1 text-lg text-blue-600 fas fa-calendar-alt dark:text-blue-400"></i>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Opening Date</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{{ park.opening_date }}</dd>
</div>
{% endif %}
{% if park.operating_season %}
<div 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 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">{{ park.opening_date }}</dd>
</div>
{% endif %}
{% if park.website %}
<div class="flex flex-col items-center justify-center text-center lg:col-span-2">
<i class="mb-1 text-lg text-blue-600 fas fa-globe dark:text-blue-400"></i>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Website</dt>
<dd class="mt-0.5">
<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="{{ 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">
Official Website
<i class="ml-1 fas fa-external-link-alt"></i>
Visit
<i class="ml-0.5 text-2xs fas fa-external-link-alt"></i>
</a>
</dd>
</div>
@@ -144,7 +128,8 @@
</div>
</div>
</div>
<!-- Photos -->
<!-- Photos Section -->
{% if park.photos.exists %}
<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>

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 %}
<div class="container px-4 mx-auto">
<!-- Header -->
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex items-start justify-between">
<div>
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">{{ ride.name }}</h1>
<p class="mb-2 text-gray-600 dark:text-gray-400">
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">
<!-- Action Buttons - Above header -->
{% if user.is_authenticated %}
<div class="flex justify-end gap-2 mb-2">
<a href="{% url 'parks:rides:ride_edit' park_slug=ride.park.slug ride_slug=ride.slug %}"
class="transition-transform btn-secondary hover:scale-105">
<i class="mr-1 fas fa-pencil-alt"></i>Edit
</a>
{% if perms.media.add_photo %}
<button class="transition-transform btn-secondary hover:scale-105"
@click="$dispatch('show-photo-upload')">
<i class="mr-1 fas fa-camera"></i>Upload Photo
</button>
{% endif %}
</div>
{% endif %}
<!-- Header Grid -->
<div class="grid grid-cols-1 gap-2 mb-8 sm:grid-cols-12">
<!-- Ride Info Card -->
<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-2xl font-bold leading-tight text-gray-900 sm:text-3xl dark:text-white">{{ ride.name }}</h1>
<div class="flex items-center justify-center mt-0.5 text-sm text-gray-600 dark:text-gray-400">
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">
{{ ride.park.name }}
</a>
{% if ride.park_area %}
- {{ ride.park_area.name }}
{% endif %}
</p>
<div class="flex flex-wrap gap-2 mt-3">
<span class="status-badge {% if ride.status == 'OPERATING' %}status-operating
</div>
<div class="flex flex-wrap items-center justify-center gap-1 mt-1">
<span class="status-badge text-xs sm:text-sm font-medium py-0.5 {% 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>
<span class="text-blue-800 bg-blue-100 status-badge dark:bg-blue-700 dark:text-blue-50">
<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-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-600 dark:text-yellow-50">
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span>
<span class="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>
{% if user.is_authenticated %}
<div class="flex gap-2">
<a href="{% url 'parks:rides:ride_edit' park_slug=ride.park.slug ride_slug=ride.slug %}" class="btn-secondary">
<i class="mr-2 fas fa-pencil-alt"></i>Edit
</a>
{% if perms.media.add_photo %}
<button class="btn-secondary" @click="$dispatch('show-photo-upload')">
<i class="mr-2 fas fa-camera"></i>Upload Photo
</button>
{% endif %}
<!-- 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 %}
{% 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>
{% 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>
<!-- Photos -->
<!-- Photos Section -->
{% if ride.photos.exists %}
<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>
@@ -60,13 +167,50 @@
</div>
{% 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 -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Column - Description and Details -->
<div class="lg:col-span-2">
{% if ride.description %}
<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">
{{ ride.description|linebreaks }}
</div>
@@ -79,8 +223,8 @@
<div class="space-y-2">
{% for name_history in ride.previous_names %}
<div class="flex justify-between">
<span>{{ name_history.name }}</span>
<span class="text-gray-500">{{ name_history.period }}</span>
<span class="text-gray-900 dark:text-white">{{ name_history.name }}</span>
<span class="text-gray-500 dark:text-gray-400">{{ name_history.period }}</span>
</div>
{% endfor %}
</div>
@@ -91,41 +235,126 @@
<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>
<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 %}
<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">
{{ coaster_stats.height_ft }} ft
</span>
</div>
{% 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 %}
<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">
{{ coaster_stats.length_ft }} ft
</span>
</div>
{% 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 %}
<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">
{{ coaster_stats.speed_mph }} mph
</span>
</div>
{% endif %}
{% if coaster_stats.ride_time_seconds %}
<div>
<span class="block text-gray-500">Inversions</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">
{{ coaster_stats.ride_time_seconds }} sec
</span>
</div>
{% 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>
{% if coaster_stats.ride_time_seconds %}
{% endif %}
{% if coaster_stats.launch_type %}
<div>
<span class="block text-gray-500">Ride Duration</span>
<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.ride_time_seconds }} sec
{{ coaster_stats.get_launch_type_display }}
</span>
</div>
{% endif %}
@@ -140,18 +369,29 @@
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Quick Facts</h2>
<dl class="space-y-4">
<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>
</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 %}
<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>
</div>
{% endif %}
{% if ride.opening_date %}
<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">
{{ ride.opening_date }}
</dd>
@@ -159,7 +399,7 @@
{% endif %}
{% if ride.status_since %}
<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">
{{ ride.status_since }}
</dd>
@@ -167,7 +407,7 @@
{% endif %}
{% if ride.closing_date %}
<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">
{{ ride.closing_date }}
</dd>
@@ -175,7 +415,7 @@
{% endif %}
{% if ride.capacity_per_hour %}
<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">
{{ ride.capacity_per_hour }} riders/hour
</dd>
@@ -183,7 +423,7 @@
{% endif %}
{% if ride.min_height_in %}
<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">
{{ ride.min_height_in }} inches
</dd>
@@ -206,65 +446,33 @@
</div>
<div class="mt-2">
{% for field, changes in record.diff_against_previous.items %}
{% if field != "updated_at" %}
<div class="text-sm">
<span class="font-medium">{{ field }}:</span>
{{ changes.old }} → {{ changes.new }}
<span class="font-medium">{{ field|title }}:</span>
<span class="text-red-600 dark:text-red-400">{{ changes.old }}</span>
<span class="mx-1"></span>
<span class="text-green-600 dark:text-green-400">{{ changes.new }}</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% empty %}
<p class="text-gray-500">No history available.</p>
<p class="text-gray-500 dark:text-gray-400">No history available.</p>
{% endfor %}
</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>
<!-- Photo Upload Modal -->
{% if perms.media.add_photo %}
<div x-data="{ show: false }"
<div x-cloak
x-data="{ show: false }"
@show-photo-upload.window="show = true"
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">
<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">

View File

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

View File

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