Standardize park and ride cards using Django Cotton components

Introduces reusable Django Cotton components for park and ride cards, standardizing their presentation and enforcing the use of the django-cotton templating system. Updates static CSS for new color variables and gradient stops.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 0bdea3fb-49ea-4863-b501-fa6f5af0cbf0
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
pac7
2025-09-22 03:21:14 +00:00
parent 0331e2087a
commit 25e6fdb496
5 changed files with 425 additions and 0 deletions

View File

@@ -54,6 +54,10 @@ outputType = "webview"
localPort = 5000 localPort = 5000
externalPort = 80 externalPort = 80
[[ports]]
localPort = 32933
externalPort = 3002
[[ports]] [[ports]]
localPort = 41923 localPort = 41923
externalPort = 3000 externalPort = 3000

View File

@@ -56,6 +56,7 @@ Migrations: All applied successfully (including circular dependency resolution)
**Spatial Data Support**: GeoDjango Point objects and spatial functionality working correctly **Spatial Data Support**: GeoDjango Point objects and spatial functionality working correctly
**CloudflareImages Integration**: Avatar functionality preserved with proper foreign key relationships **CloudflareImages Integration**: Avatar functionality preserved with proper foreign key relationships
**Django-Cotton Integration**: Modern component-based template system with EXACT visual preservation **Django-Cotton Integration**: Modern component-based template system with EXACT visual preservation
**Cotton Components**: Standardized park_card.html and ride_card.html components with full feature support
### API Endpoints Available ### API Endpoints Available
- `/api/v1/parks/` - Parks API with spatial data - `/api/v1/parks/` - Parks API with spatial data

View File

@@ -22,6 +22,7 @@
--color-orange-200: oklch(90.1% 0.076 70.697); --color-orange-200: oklch(90.1% 0.076 70.697);
--color-orange-600: oklch(64.6% 0.222 41.116); --color-orange-600: oklch(64.6% 0.222 41.116);
--color-orange-800: oklch(47% 0.157 37.304); --color-orange-800: oklch(47% 0.157 37.304);
--color-orange-900: oklch(40.8% 0.123 38.172);
--color-amber-100: oklch(96.2% 0.059 95.617); --color-amber-100: oklch(96.2% 0.059 95.617);
--color-amber-800: oklch(47.3% 0.137 46.201); --color-amber-800: oklch(47.3% 0.137 46.201);
--color-yellow-50: oklch(98.7% 0.026 102.212); --color-yellow-50: oklch(98.7% 0.026 102.212);
@@ -1730,6 +1731,10 @@
--tw-gradient-to: var(--color-blue-50); --tw-gradient-to: var(--color-blue-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
} }
.to-blue-600 {
--tw-gradient-to: var(--color-blue-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-gray-100 { .to-gray-100 {
--tw-gradient-to: var(--color-gray-100); --tw-gradient-to: var(--color-gray-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -3907,6 +3912,11 @@
} }
} }
} }
.dark\:bg-orange-900 {
@media (prefers-color-scheme: dark) {
background-color: var(--color-orange-900);
}
}
.dark\:bg-purple-800 { .dark\:bg-purple-800 {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
background-color: var(--color-purple-800); background-color: var(--color-purple-800);
@@ -4239,6 +4249,11 @@
color: var(--color-green-900); color: var(--color-green-900);
} }
} }
.dark\:text-orange-200 {
@media (prefers-color-scheme: dark) {
color: var(--color-orange-200);
}
}
.dark\:text-primary { .dark\:text-primary {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
color: var(--color-primary); color: var(--color-primary);

View File

@@ -0,0 +1,190 @@
{% comment %}
Park Card Component - Django Cotton Version
A reusable park card component that supports both list and grid view modes.
Includes status badges, operator information, description, and ride/coaster statistics.
Usage Examples:
List View:
<c-park_card
park=park
view_mode="list"
/>
Grid View:
<c-park_card
park=park
view_mode="grid"
/>
With custom CSS classes:
<c-park_card
park=park
view_mode="grid"
class="custom-class"
/>
Parameters:
- park: Park object (required)
- view_mode: "list" or "grid" (default: "grid")
- class: Additional CSS classes (optional)
Features:
- Responsive design with hover effects
- Status badge with proper color coding
- Operator information display
- Description with automatic truncation (30 words for list, 15 for grid)
- Ride and coaster count statistics with icons
- Gradient effects and modern styling
- Links to park detail pages
{% endcomment %}
<c-vars
park=""
view_mode="grid"
class=""
/>
{% if park %}
{% if view_mode == 'list' %}
{# Enhanced List View Item #}
<article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-[1.02] overflow-hidden {{ class }}">
<div class="p-6">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
{# Main Content Section #}
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between mb-3">
<h2 class="text-xl lg:text-2xl font-bold">
<a href="{% url 'parks:park_detail' park.slug %}"
class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent hover:from-blue-600 hover:to-purple-600 dark:hover:from-blue-400 dark:hover:to-purple-400 transition-all duration-300">
{{ park.name }}
</a>
</h2>
{# Status Badge #}
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border
{% if park.status == 'operating' %}bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800
{% elif park.status == 'closed' %}bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800
{% elif park.status == 'seasonal' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800
{% elif park.status == 'closed_temp' %}bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800
{% elif park.status == 'closed_permanently' or park.status == 'closed_perm' %}bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800
{% elif park.status == 'under_construction' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800
{% else %}bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600{% endif %}">
{{ park.get_status_display }}
</span>
</div>
{% if park.operator %}
<div class="text-base font-medium text-gray-600 dark:text-gray-400 mb-3">
{{ park.operator.name }}
</div>
{% endif %}
{% if park.description %}
<p class="text-gray-600 dark:text-gray-400 line-clamp-2 mb-4">
{{ park.description|truncatewords:30 }}
</p>
{% endif %}
</div>
{# Stats Section #}
{% if park.ride_count or park.coaster_count %}
<div class="flex items-center space-x-6 text-sm">
{% if park.ride_count %}
<div class="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg border border-blue-200/50 dark:border-blue-800/50">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<span class="font-semibold text-blue-700 dark:text-blue-300">{{ park.ride_count }}</span>
<span class="text-blue-600 dark:text-blue-400">rides</span>
</div>
{% endif %}
{% if park.coaster_count %}
<div class="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-lg border border-purple-200/50 dark:border-purple-800/50">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span class="font-semibold text-purple-700 dark:text-purple-300">{{ park.coaster_count }}</span>
<span class="text-purple-600 dark:text-purple-400">coasters</span>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</article>
{% else %}
{# Enhanced Grid View Item #}
<article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 hover:-rotate-1 overflow-hidden {{ class }}">
{# Card Header with Gradient #}
<div class="h-2 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500"></div>
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<h2 class="text-xl font-bold line-clamp-2 flex-1 mr-3">
<a href="{% url 'parks:park_detail' park.slug %}"
class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent hover:from-blue-600 hover:to-purple-600 dark:hover:from-blue-400 dark:hover:to-purple-400 transition-all duration-300">
{{ park.name }}
</a>
</h2>
{# Status Badge #}
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold border shrink-0
{% if park.status == 'operating' %}bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800
{% elif park.status == 'closed' %}bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800
{% elif park.status == 'seasonal' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800
{% elif park.status == 'closed_temp' %}bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800
{% elif park.status == 'closed_permanently' or park.status == 'closed_perm' %}bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800
{% elif park.status == 'under_construction' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800
{% else %}bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600{% endif %}">
{{ park.get_status_display }}
</span>
</div>
{% if park.operator %}
<div class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-3 truncate">
{{ park.operator.name }}
</div>
{% endif %}
{% if park.description %}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-3 mb-4">
{{ park.description|truncatewords:15 }}
</p>
{% endif %}
{# Stats Footer #}
{% if park.ride_count or park.coaster_count %}
<div class="flex items-center justify-between pt-4 border-t border-gray-200/50 dark:border-gray-600/50">
<div class="flex items-center space-x-4 text-sm">
{% if park.ride_count %}
<div class="flex items-center space-x-1 text-blue-600 dark:text-blue-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<span class="font-semibold">{{ park.ride_count }}</span>
</div>
{% endif %}
{% if park.coaster_count %}
<div class="flex items-center space-x-1 text-purple-600 dark:text-purple-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span class="font-semibold">{{ park.coaster_count }}</span>
</div>
{% endif %}
</div>
{# View Details Arrow #}
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</div>
{% endif %}
</div>
</article>
{% endif %}
{% endif %}

View File

@@ -0,0 +1,215 @@
{% comment %}
Ride Card Component - Django Cotton Version
A comprehensive ride card component with image handling, status badges, and feature displays.
Includes all ride statistics, special features, and manufacturer information.
Usage Examples:
Basic usage:
<c-ride_card ride=ride />
With custom CSS classes:
<c-ride_card
ride=ride
class="custom-class"
/>
With custom image fallback:
<c-ride_card
ride=ride
fallback_gradient="from-red-500 to-blue-600"
/>
Parameters:
- ride: Ride object (required)
- class: Additional CSS classes (optional)
- fallback_gradient: Custom gradient for image fallback (default: "from-blue-500 to-purple-600")
Features:
- Image handling with gradient fallback backgrounds
- Status badges with proper color coding (operating, closed_temporarily, closed_permanently, under_construction)
- Ride name with link to detail page
- Category and park information display
- Statistics grid for height, speed, capacity, duration
- Special features badges (inversions, launches, track_type)
- Opening date and manufacturer/designer information
- Responsive design with hover effects
- Modern Tailwind styling and animations
{% endcomment %}
<c-vars
ride=""
class=""
fallback_gradient="from-blue-500 to-purple-600"
/>
{% if ride %}
<div class="ride-card bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-lg transition-all duration-300 {{ class }}">
<!-- Ride image -->
<div class="relative h-48 bg-gradient-to-br {{ fallback_gradient }}">
{% if ride.image %}
<img src="{{ ride.image.url }}"
alt="{{ ride.name }}"
class="w-full h-full object-cover">
{% else %}
<div class="flex items-center justify-center h-full">
<i class="fas fa-rocket text-4xl text-white opacity-50"></i>
</div>
{% endif %}
<!-- Status badge -->
<div class="absolute top-3 right-3">
{% if ride.operating_status == 'operating' or ride.operating_status == 'OPERATING' %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
<i class="fas fa-play-circle mr-1"></i>
Operating
</span>
{% elif ride.operating_status == 'closed_temporarily' or ride.operating_status == 'CLOSED_TEMP' %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
<i class="fas fa-pause-circle mr-1"></i>
Temporarily Closed
</span>
{% elif ride.operating_status == 'closed_permanently' or ride.operating_status == 'CLOSED_PERM' %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
<i class="fas fa-stop-circle mr-1"></i>
Permanently Closed
</span>
{% elif ride.operating_status == 'under_construction' or ride.operating_status == 'UNDER_CONSTRUCTION' %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
<i class="fas fa-hard-hat mr-1"></i>
Under Construction
</span>
{% elif ride.operating_status == 'sbno' or ride.operating_status == 'SBNO' %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
<i class="fas fa-pause-circle mr-1"></i>
SBNO
</span>
{% endif %}
</div>
</div>
<!-- Ride details -->
<div class="p-5">
<!-- Name and category -->
<div class="mb-3">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
<a href="{% url 'rides:ride_detail' ride.id %}"
class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
{{ ride.name }}
</a>
</h3>
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 mr-2">
{{ ride.category|default:"Ride" }}
</span>
{% if ride.park %}
<span class="flex items-center">
<i class="fas fa-map-marker-alt mr-1"></i>
{{ ride.park.name }}
</span>
{% endif %}
</div>
</div>
<!-- Key stats grid -->
<div class="grid grid-cols-2 gap-3 mb-4">
{% if ride.height %}
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.height }}ft</div>
<div class="text-xs text-gray-600 dark:text-gray-400">Height</div>
</div>
{% endif %}
{% if ride.rollercoaster_stats.max_speed %}
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.rollercoaster_stats.max_speed }}mph</div>
<div class="text-xs text-gray-600 dark:text-gray-400">Top Speed</div>
</div>
{% elif ride.max_speed %}
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.max_speed }}mph</div>
<div class="text-xs text-gray-600 dark:text-gray-400">Max Speed</div>
</div>
{% endif %}
{% if ride.capacity_per_hour %}
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.capacity_per_hour }}</div>
<div class="text-xs text-gray-600 dark:text-gray-400">Capacity/Hr</div>
</div>
{% endif %}
{% if ride.duration %}
<div class="text-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div class="text-lg font-semibold text-gray-900 dark:text-white">{{ ride.duration }}s</div>
<div class="text-xs text-gray-600 dark:text-gray-400">Duration</div>
</div>
{% endif %}
</div>
<!-- Special features -->
{% if ride.has_inversions or ride.has_launches or ride.rollercoaster_stats.track_type or ride.track_type %}
<div class="flex flex-wrap gap-1 mb-3">
{% if ride.has_inversions %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
<i class="fas fa-sync-alt mr-1"></i>
{% if ride.rollercoaster_stats.number_of_inversions %}
{{ ride.rollercoaster_stats.number_of_inversions }} Inversion{{ ride.rollercoaster_stats.number_of_inversions|pluralize }}
{% else %}
Inversions
{% endif %}
</span>
{% endif %}
{% if ride.has_launches %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
<i class="fas fa-rocket mr-1"></i>
{% if ride.rollercoaster_stats.number_of_launches %}
{{ ride.rollercoaster_stats.number_of_launches }} Launch{{ ride.rollercoaster_stats.number_of_launches|pluralize }}
{% else %}
Launched
{% endif %}
</span>
{% endif %}
{% if ride.rollercoaster_stats.track_type %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{{ ride.rollercoaster_stats.track_type|title }}
</span>
{% elif ride.track_type %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{{ ride.track_type|title }}
</span>
{% endif %}
</div>
{% endif %}
<!-- Opening date -->
{% if ride.opened_date %}
<div class="text-sm text-gray-600 dark:text-gray-400 mb-3">
<i class="fas fa-calendar mr-1"></i>
Opened {{ ride.opened_date|date:"F j, Y" }}
</div>
{% endif %}
<!-- Manufacturer and designer -->
{% if ride.manufacturer or ride.designer %}
<div class="text-sm text-gray-600 dark:text-gray-400">
{% if ride.manufacturer %}
<div class="flex items-center mb-1">
<i class="fas fa-industry mr-1"></i>
<span>{{ ride.manufacturer.name }}</span>
</div>
{% endif %}
{% if ride.designer and ride.designer != ride.manufacturer %}
<div class="flex items-center">
<i class="fas fa-drafting-compass mr-1"></i>
<span>Designed by {{ ride.designer.name }}</span>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endif %}