diff --git a/.clinerules/thrillwiki-simple.md b/.clinerules/thrillwiki-simple.md index f27b9f9c..44df4dbc 100644 --- a/.clinerules/thrillwiki-simple.md +++ b/.clinerules/thrillwiki-simple.md @@ -12,8 +12,8 @@ tags: ["django", "architecture", "context7-integration", "thrillwiki"] Theme park database platform with Django REST Framework serving 120+ API endpoints for parks, rides, companies, and users. ## Core Architecture -- **Backend**: Django 5.0+, DRF, PostgreSQL+PostGIS, Redis, Celery -- **Frontend**: HTMX + AlpineJS + Tailwind CSS + Django-Cotton +- **Backend**: Django 5.1+, DRF, PostgreSQL+PostGIS, Redis, Celery +- **Frontend**: HTMX (V2+) + AlpineJS + Tailwind CSS (V4+) + Django-Cotton - 🚨 **ABSOLUTELY NO Custom JS** - use HTMX + AlpineJS ONLY - Clean, simple UX preferred - **Media**: Cloudflare Images with Direct Upload diff --git a/apps/core/urls/search.py b/apps/core/urls/search.py index 643ff830..7c492d15 100644 --- a/apps/core/urls/search.py +++ b/apps/core/urls/search.py @@ -4,6 +4,7 @@ from apps.core.views.search import ( FilterFormView, LocationSearchView, LocationSuggestionsView, + AdvancedSearchView, ) from apps.rides.views import RideSearchView @@ -12,6 +13,7 @@ app_name = "search" urlpatterns = [ path("parks/", AdaptiveSearchView.as_view(), name="search"), path("parks/filters/", FilterFormView.as_view(), name="filter_form"), + path("advanced/", AdvancedSearchView.as_view(), name="advanced"), path("rides/", RideSearchView.as_view(), name="ride_search"), path("rides/results/", RideSearchView.as_view(), name="ride_search_results"), # Location-aware search diff --git a/apps/core/views/search.py b/apps/core/views/search.py index 74affe3b..d9c6dae0 100644 --- a/apps/core/views/search.py +++ b/apps/core/views/search.py @@ -176,3 +176,43 @@ class LocationSuggestionsView(TemplateView): return JsonResponse({"suggestions": suggestions}) except Exception as e: return JsonResponse({"error": str(e)}, status=500) + + +class AdvancedSearchView(TemplateView): + """Advanced search view with comprehensive filtering options for both parks and rides""" + template_name = "core/search/advanced.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # Import here to avoid circular imports + from apps.parks.filters import ParkFilter + from apps.rides.filters import RideFilter + from apps.parks.models import Park + from apps.rides.models.rides import Ride + + # Initialize filtersets for both parks and rides + park_filterset = ParkFilter(self.request.GET, queryset=Park.objects.all()) + ride_filterset = RideFilter(self.request.GET, queryset=Ride.objects.all()) + + # Determine what type of search to show based on request parameters + search_type = self.request.GET.get('search_type', 'parks') # Default to parks + + context.update({ + 'page_title': 'Advanced Search', + 'page_description': 'Find exactly what you\'re looking for with our comprehensive search filters.', + 'search_type': search_type, + 'park_filters': park_filterset, + 'ride_filters': ride_filterset, + 'park_results': park_filterset.qs if search_type == 'parks' else None, + 'ride_results': ride_filterset.qs if search_type == 'rides' else None, + 'has_filters': bool(self.request.GET), + }) + + return context + + def get_template_names(self): + """Return appropriate template for HTMX requests""" + if hasattr(self.request, 'htmx') and self.request.htmx: + return ["core/search/partials/advanced_results.html"] + return [self.template_name] diff --git a/apps/parks/urls.py b/apps/parks/urls.py index e4950655..b10c81bd 100644 --- a/apps/parks/urls.py +++ b/apps/parks/urls.py @@ -15,6 +15,7 @@ app_name = "parks" urlpatterns = [ # Park views with autocomplete search path("", views.ParkListView.as_view(), name="park_list"), + path("trending/", views.TrendingParksView.as_view(), name="trending"), path("operators/", views.OperatorListView.as_view(), name="operator_list"), path("create/", views.ParkCreateView.as_view(), name="park_create"), # Add park button endpoint (moved before park detail pattern) diff --git a/apps/parks/views.py b/apps/parks/views.py index 2bd79764..ddf2a91e 100644 --- a/apps/parks/views.py +++ b/apps/parks/views.py @@ -29,6 +29,9 @@ from django.urls import reverse from django.shortcuts import get_object_or_404, render from decimal import InvalidOperation from django.views.generic import DetailView, ListView, CreateView, UpdateView +from django.db.models import Count, Avg, Q +from django.utils import timezone +from datetime import timedelta import requests from decimal import Decimal, ROUND_DOWN from typing import Any, Optional, cast, Literal, Dict @@ -224,6 +227,56 @@ def reverse_geocode(request: HttpRequest) -> JsonResponse: return JsonResponse({"error": "Geocoding failed"}, status=500) +class TrendingParksView(ListView): + """View for displaying trending/popular parks""" + model = Park + template_name = "parks/trending_parks.html" + context_object_name = "parks" + paginate_by = 20 + + def get_queryset(self) -> QuerySet[Park]: + """Get trending parks based on ride count, ratings, and recent activity""" + # For now, order by a combination of factors that indicate popularity: + # 1. Parks with more rides + # 2. Higher average ratings + # 3. More recent activity (reviews, photos, etc.) + thirty_days_ago = timezone.now() - timedelta(days=30) + + return ( + get_base_park_queryset() + .annotate( + recent_reviews=Count( + 'reviews', + filter=Q(reviews__created_at__gte=thirty_days_ago) + ), + recent_photos=Count( + 'photos', + filter=Q(photos__created_at__gte=thirty_days_ago) + ) + ) + .order_by( + '-recent_reviews', + '-recent_photos', + '-ride_count', + '-average_rating' + ) + ) + + def get_template_names(self) -> list[str]: + """Return appropriate template for HTMX requests""" + if self.request.htmx: + return ["parks/partials/trending_parks.html"] + return [self.template_name] + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + context = super().get_context_data(**kwargs) + context.update({ + 'page_title': 'Trending Parks', + 'page_description': 'Discover the most popular theme parks with recent activity and high ratings.' + }) + return context + + class ParkListView(HTMXFilterableMixin, ListView): model = Park template_name = "parks/enhanced_park_list.html" diff --git a/apps/rides/urls.py b/apps/rides/urls.py index 27f833bc..b12ebfb3 100644 --- a/apps/rides/urls.py +++ b/apps/rides/urls.py @@ -6,6 +6,7 @@ app_name = "rides" urlpatterns = [ # Global list views path("", views.RideListView.as_view(), name="global_ride_list"), + path("new/", views.NewRidesView.as_view(), name="new"), # Global category views path( "roller-coasters/", diff --git a/apps/rides/views.py b/apps/rides/views.py index 09cf0d26..d58982a8 100644 --- a/apps/rides/views.py +++ b/apps/rides/views.py @@ -302,6 +302,37 @@ class RideListView(ListView): return context +class NewRidesView(ListView): + """View for displaying recently added rides""" + model = Ride + template_name = "rides/new_rides.html" + context_object_name = "rides" + paginate_by = 20 + + def get_queryset(self): + """Get recently added rides, ordered by creation date""" + return ( + Ride.objects.all() + .select_related("park", "ride_model", "ride_model__manufacturer") + .prefetch_related("photos") + .order_by("-created_at") + ) + + def get_template_names(self): + """Return appropriate template for HTMX requests""" + if hasattr(self.request, "htmx") and self.request.htmx: + return ["rides/partials/new_rides.html"] + return [self.template_name] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'page_title': 'New Attractions', + 'page_description': 'Discover the latest rides and attractions added to theme parks around the world.' + }) + return context + + class SingleCategoryListView(ListView): """View for displaying rides of a specific category""" diff --git a/templates/base/base.html b/templates/base/base.html index 0ad145ab..0cc513bb 100644 --- a/templates/base/base.html +++ b/templates/base/base.html @@ -48,7 +48,6 @@ {% block critical_resources %} - {% endblock %} @@ -62,10 +61,10 @@ /> - + - + @@ -173,40 +172,60 @@ -
t.id === id); - if (toast) { - toast.visible = false; - setTimeout(() => { - this.toasts = this.toasts.filter(t => t.id !== id); - }, 300); + }); + + // Initialize Alpine stores + Alpine.store('app', { + user: null, + theme: localStorage.getItem('theme') || 'system', + searchQuery: '', + notifications: [] + }); + + Alpine.store('toast', { + toasts: [], + show(message, type = 'info', duration = 5000) { + const id = Date.now() + Math.random(); + const toast = { id, message, type, visible: true, progress: 100 }; + this.toasts.push(toast); + if (duration > 0) { + setTimeout(() => this.hide(id), duration); + } + return id; + }, + hide(id) { + const toast = this.toasts.find(t => t.id === id); + if (toast) { + toast.visible = false; + setTimeout(() => { + this.toasts = this.toasts.filter(t => t.id !== id); + }, 300); + } } - } + }); }); - " style="display: none;">
+ {% block extra_js %}{% endblock %} diff --git a/templates/components/layout/enhanced_header.html b/templates/components/layout/enhanced_header.html index b3c73ec5..160b455d 100644 --- a/templates/components/layout/enhanced_header.html +++ b/templates/components/layout/enhanced_header.html @@ -51,9 +51,9 @@ - @@ -367,7 +367,7 @@ Parks - diff --git a/templates/home.html b/templates/home.html index b61d1563..8ce9b38e 100644 --- a/templates/home.html +++ b/templates/home.html @@ -98,46 +98,67 @@ -
- +
+
-
-
-
-
-
+
+ +
+
+

Explore Amazing Parks

+

+ Discover incredible theme parks from around the world with detailed guides and insider tips. +

-
-
+ Featured +
-
-
-
-
-
+
+ +
+
+

Thrilling Rides

+

+ From heart-pounding roller coasters to magical dark rides, find your next adventure. +

-
-
+ Popular +
-
-
-
-
-
+
+ +
+
+

Advanced Search

+

+ Find exactly what you're looking for with our powerful search and filtering tools. +

-
-
+ Tools +