mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 13:31:08 -05:00
add category views for each type of ride, add ride designers
This commit is contained in:
Binary file not shown.
@@ -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
0
designers/__init__.py
Normal file
10
designers/admin.py
Normal file
10
designers/admin.py
Normal 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
6
designers/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DesignersConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "designers"
|
||||
88
designers/migrations/0001_initial.py
Normal file
88
designers/migrations/0001_initial.py
Normal 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),
|
||||
),
|
||||
]
|
||||
0
designers/migrations/__init__.py
Normal file
0
designers/migrations/__init__.py
Normal file
37
designers/models.py
Normal file
37
designers/models.py
Normal 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
3
designers/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
8
designers/urls.py
Normal file
8
designers/urls.py
Normal 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
29
designers/views.py
Normal 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
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@@ -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")),
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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'
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -43,10 +43,19 @@ class Ride(models.Model):
|
||||
default='OT'
|
||||
)
|
||||
manufacturer = models.ForeignKey(
|
||||
'companies.manufacturer', on_delete=models.CASCADE, null=False, blank=False
|
||||
)
|
||||
# other fields...
|
||||
|
||||
'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'
|
||||
)
|
||||
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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
{% 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
|
||||
</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>
|
||||
<!-- 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="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="transition-transform btn-secondary hover:scale-105">
|
||||
<i class="mr-1 fas fa-edit"></i>Edit
|
||||
</a>
|
||||
{% 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 }}
|
||||
<!-- 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>
|
||||
<div class="mt-1 text-gray-600 dark:text-gray-400">Theme Parks</div>
|
||||
{% endif %}
|
||||
</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 }}
|
||||
|
||||
<!-- 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>
|
||||
<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 %}
|
||||
|
||||
<!-- 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 class="mt-1 text-gray-600 dark:text-gray-400">Total Attractions</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 -->
|
||||
<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>
|
||||
|
||||
@@ -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">
|
||||
{% 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
|
||||
</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>
|
||||
<!-- 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="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="transition-transform btn-secondary hover:scale-105">
|
||||
<i class="mr-1 fas fa-edit"></i>Edit
|
||||
</a>
|
||||
{% 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 }}
|
||||
<!-- 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>
|
||||
<div class="mt-1 text-gray-600 dark:text-gray-400">Total Rides</div>
|
||||
{% endif %}
|
||||
</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 }}
|
||||
|
||||
<!-- 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>
|
||||
<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 }}
|
||||
|
||||
<!-- 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 class="mt-1 text-gray-600 dark:text-gray-400">Parks with Rides</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 -->
|
||||
<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>
|
||||
|
||||
121
templates/designers/designer_detail.html
Normal file
121
templates/designers/designer_detail.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
154
templates/rides/ride_category_list.html
Normal file
154
templates/rides/ride_category_list.html
Normal 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 %}
|
||||
@@ -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">
|
||||
{{ 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
|
||||
{% 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">
|
||||
{{ 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>
|
||||
{{ ride.average_rating|floatformat:1 }}/10
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- 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 %}
|
||||
</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>
|
||||
|
||||
<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 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 %}
|
||||
{% 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 %}
|
||||
{% 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,44 +235,129 @@
|
||||
<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 %}
|
||||
<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 %}
|
||||
<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">
|
||||
{{ 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>
|
||||
{% 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>
|
||||
{% 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 %}
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">{{ field }}:</span>
|
||||
{{ changes.old }} → {{ changes.new }}
|
||||
</div>
|
||||
{% if field != "updated_at" %}
|
||||
<div class="text-sm">
|
||||
<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">
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -45,6 +45,7 @@ INSTALLED_APPS = [
|
||||
"media.apps.MediaConfig",
|
||||
"moderation",
|
||||
"history_tracking",
|
||||
"designers",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user