diff --git a/core/templates/base.html b/core/templates/base.html new file mode 100644 index 00000000..a377037d --- /dev/null +++ b/core/templates/base.html @@ -0,0 +1,324 @@ +{% load static %} + + + + + + + {% block title %}ThrillWiki{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + {% block extra_head %}{% endblock %} + + + +
+ +
+ + + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + + +
+ {% block content %}{% endblock %} +
+ + + + + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/rides/templates/rides/partials/search_suggestions.html b/rides/templates/rides/partials/search_suggestions.html new file mode 100644 index 00000000..520e49d2 --- /dev/null +++ b/rides/templates/rides/partials/search_suggestions.html @@ -0,0 +1,26 @@ +{% if suggestions %} +
+ {% for suggestion in suggestions %} +
+ {% if suggestion.type == 'ride' %} + 🎢 + {{ suggestion.text }} + ({{ suggestion.count }} rides) + {% elif suggestion.type == 'park' %} + 🎪 + {{ suggestion.text }} + {% if suggestion.location %} + {{ suggestion.location }} + {% endif %} + {% elif suggestion.type == 'category' %} + 📂 + {{ suggestion.text }} + ({{ suggestion.count }} rides) + {% endif %} +
+ {% endfor %} +
+{% endif %} \ No newline at end of file diff --git a/rides/templates/rides/ride_list.html b/rides/templates/rides/ride_list.html new file mode 100644 index 00000000..997cda25 --- /dev/null +++ b/rides/templates/rides/ride_list.html @@ -0,0 +1,214 @@ +{% extends "base/base.html" %} +{% load static %} +{% load ride_tags %} + +{% block title %} + {% if park %} + Rides at {{ park.name }} - ThrillWiki + {% else %} + All Rides - ThrillWiki + {% endif %} +{% endblock %} + +{% block content %} +
+
+

+ {% if park %} + Rides at {{ park.name }} + {% else %} + All Rides + {% endif %} +

+ + {# Search Section #} +
+
+ + +
+ +
+
+ + {# Search Suggestions #} +
+
+
+ + {# Quick Filter Buttons #} +
+ + + {% for code, name in category_choices %} + + {% endfor %} +
+ + {# Active Filter Tags #} +
+ {% if request.GET.q %} + + Search: {{ request.GET.q }} + + + {% endif %} + {% if request.GET.category %} + + Category: {{ request.GET.category|get_category_display }} + + + {% endif %} + {% if request.GET.operating %} + + Operating Only + + + {% endif %} +
+
+ + {# Results Section #} +
+ {% include "rides/partials/ride_list_results.html" %} +
+
+{% endblock %} + +{% block extra_js %} + + + +{% endblock %} \ No newline at end of file diff --git a/rides/templatetags/ride_tags.py b/rides/templatetags/ride_tags.py index d6da8801..96f58316 100644 --- a/rides/templatetags/ride_tags.py +++ b/rides/templatetags/ride_tags.py @@ -1,5 +1,6 @@ from django import template from django.templatetags.static import static +from ..models import CATEGORY_CHOICES register = template.Library() @@ -22,3 +23,9 @@ def get_ride_placeholder_image(category): def get_park_placeholder_image(): """Return placeholder image for parks""" return static("images/placeholders/default-park.jpg") + + +@register.filter +def get_category_display(code): + """Convert category code to display name""" + return dict(CATEGORY_CHOICES).get(code, code) diff --git a/rides/views.py b/rides/views.py index 4362cb7e..a24381de 100644 --- a/rides/views.py +++ b/rides/views.py @@ -219,25 +219,55 @@ class RideListView(ListView): context_object_name = 'rides' def get_queryset(self): - """Get all rides or filter by park if park_slug is provided""" + """Get filtered rides based on search and filters""" queryset = Ride.objects.all().select_related( 'park', 'ride_model', 'ride_model__manufacturer' ).prefetch_related('photos') + # Park filter if 'park_slug' in self.kwargs: self.park = get_object_or_404(Park, slug=self.kwargs['park_slug']) queryset = queryset.filter(park=self.park) + # Search term handling + search = self.request.GET.get('q', '').strip() + if search: + # Split search terms for more flexible matching + search_terms = search.split() + search_query = Q() + + for term in search_terms: + term_query = Q( + name__icontains=term + ) | Q( + park__name__icontains=term + ) | Q( + description__icontains=term + ) + search_query &= term_query + + queryset = queryset.filter(search_query) + + # Category filter + category = self.request.GET.get('category') + if category and category != 'all': + queryset = queryset.filter(category=category) + + # Operating status filter + if self.request.GET.get('operating') == 'true': + queryset = queryset.filter(status='operating') + return queryset def get_context_data(self, **kwargs): - """Add park to context if park_slug is provided""" + """Add park and category choices to context""" context = super().get_context_data(**kwargs) if hasattr(self, 'park'): context['park'] = self.park context['park_slug'] = self.kwargs['park_slug'] + context['category_choices'] = CATEGORY_CHOICES return context @@ -336,3 +366,63 @@ def search_ride_models(request: HttpRequest) -> HttpResponse: {"ride_models": ride_models, "search_term": query, "manufacturer_id": manufacturer_id}, ) + + +def get_search_suggestions(request: HttpRequest) -> HttpResponse: + """Get smart search suggestions for rides + + Returns suggestions including: + - Common matching ride names + - Matching parks + - Matching categories + """ + query = request.GET.get('q', '').strip().lower() + suggestions = [] + + if query: + # Get common ride names + matching_names = Ride.objects.filter( + name__icontains=query + ).values('name').annotate( + count=Count('id') + ).order_by('-count')[:3] + + for match in matching_names: + suggestions.append({ + 'type': 'ride', + 'text': match['name'], + 'count': match['count'] + }) + + # Get matching parks + matching_parks = Park.objects.filter( + Q(name__icontains=query) | + Q(location__city__icontains=query) + )[:3] + + for park in matching_parks: + suggestions.append({ + 'type': 'park', + 'text': park.name, + 'location': park.location.city if park.location else None + }) + + # Add category matches + for code, name in CATEGORY_CHOICES: + if query in name.lower(): + ride_count = Ride.objects.filter(category=code).count() + suggestions.append({ + 'type': 'category', + 'code': code, + 'text': name, + 'count': ride_count + }) + + return render( + request, + 'rides/partials/search_suggestions.html', + { + 'suggestions': suggestions, + 'query': query + } + ) diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 00000000..a377037d --- /dev/null +++ b/templates/base.html @@ -0,0 +1,324 @@ +{% load static %} + + + + + + + {% block title %}ThrillWiki{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + {% block extra_head %}{% endblock %} + + + +
+ +
+ + + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + + +
+ {% block content %}{% endblock %} +
+ + + + + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/templates/rides/ride_list.html b/templates/rides/ride_list.html deleted file mode 100644 index 42273942..00000000 --- a/templates/rides/ride_list.html +++ /dev/null @@ -1,218 +0,0 @@ -{% extends 'base/base.html' %} -{% load static %} -{% load ride_tags %} - -{% block title %} - {% if park %} - Rides at {{ park.name }} - ThrillWiki - {% else %} - All Rides - ThrillWiki - {% endif %} -{% endblock %} - -{% block content %} -
-
-
- {% if park %} -

Rides at {{ park.name }}

- - Back to {{ park.name }} - - {% else %} -

All Rides

- {% endif %} -
- {% if user.is_authenticated %} - {% if park %} - - - - - Add Ride - - {% endif %} - {% endif %} -
- - -
- Quick Filters: - {% for filter in quick_filters %} - - {% endfor %} - -
- - - - - Updating results... - -
-
- - -
- - {% if current_filters.search or current_filters.category or current_filters.status %} -
- Active Filters: - {% for name, value in current_filters.items %} - {% if value %} - - {% endif %} - {% endfor %} -
- {% endif %} - -
- -
-
- - -
-
-
-
- - -
- - -
- -
- - -
- - -
- -
- - -
-
- - -
-
- - -
-
- - -
- - -
- - -
-