Implement ride count fields with real-time annotations; update filters and templates for consistency and accuracy

This commit is contained in:
pacnpal
2025-02-13 16:44:30 -05:00
parent 9d6f6dab2c
commit c19aaf2f4b
10 changed files with 988 additions and 367 deletions

View File

@@ -2,136 +2,97 @@
{% load static %}
{% load filter_utils %}
{% block page_title %}Parks{% endblock %}
{% block title %}Parks - ThrillWiki{% endblock %}
{% block filter_errors %}
{% if filter.errors %}
<div class="bg-red-50 border-l-4 border-red-400 p-4 mb-4">
<h3 class="text-sm font-medium text-red-800">Please correct the following errors:</h3>
<ul class="mt-2 text-sm text-red-700 list-disc pl-5 space-y-1">
{% for field, errors in filter.errors.items %}
{% for error in errors %}
<li>{{ field }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block list_header %}
<div class="flex items-center space-x-2">
<button hx-get="{% url 'parks:park_list' %}?view_mode=grid" hx-target="#results-container" hx-push-url="true" class="p-2 rounded {% if request.GET.view_mode == 'grid' or not request.GET.view_mode %}bg-gray-200{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
<button hx-get="{% url 'parks:park_list' %}?view_mode=list" hx-target="#results-container" hx-push-url="true" class="p-2 rounded {% if request.GET.view_mode == 'list' %}bg-gray-200{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/></svg>
</button>
</div>
{% block list_actions %}
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900 mr-4">Parks</h1>
<div class="flex items-center space-x-4">
<h1 class="text-2xl font-bold text-gray-900">Parks</h1>
<div class="flex items-center space-x-2 bg-gray-100 rounded-lg p-1" role="group" aria-label="View mode selection">
<button hx-get="{% url 'parks:park_list' %}?view_mode=grid{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
hx-target="#park-results"
hx-push-url="true"
class="p-2 rounded transition-colors duration-200 {% if request.GET.view_mode == 'grid' or not request.GET.view_mode %}bg-white shadow-sm{% endif %}"
aria-label="Grid view"
aria-pressed="{% if request.GET.view_mode == 'grid' or not request.GET.view_mode %}true{% else %}false{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
</button>
<button hx-get="{% url 'parks:park_list' %}?view_mode=list{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
hx-target="#park-results"
hx-push-url="true"
class="p-2 rounded transition-colors duration-200 {% if request.GET.view_mode == 'list' %}bg-white shadow-sm{% endif %}"
aria-label="List view"
aria-pressed="{% if request.GET.view_mode == 'list' %}true{% else %}false{% endif %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h7"/>
</svg>
</button>
</div>
</div>
{% if user.is_authenticated %}
<a href="{% url 'parks:park_create' %}" class="btn btn-primary">
<a href="{% url 'parks:park_create' %}"
class="btn btn-primary"
data-testid="add-park-button">
Add Park
</a>
{% endif %}
</div>
{% endblock %}
{% block list_description %}
Browse and filter amusement parks, theme parks, and water parks from around the world.
{% endblock %}
{% block filter_section %}
<div class="mb-6">
{# Quick Search #}
<div class="mb-8">
<div class="max-w-3xl mx-auto"
x-data="{
selectedIndex: -1,
results: [],
get hasResults() { return this.results.length > 0 },
init() {
this.$watch('results', () => this.selectedIndex = -1)
},
onKeyDown(e) {
if (!this.hasResults) return
if (e.key === 'ArrowDown') {
e.preventDefault()
this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1)
}
else if (e.key === 'ArrowUp') {
e.preventDefault()
this.selectedIndex = Math.max(this.selectedIndex - 1, 0)
}
else if (e.key === 'Enter' && this.selectedIndex >= 0) {
e.preventDefault()
this.$refs[`result-${this.selectedIndex}`]?.click()
}
else if (e.key === 'Escape') {
this.results = []
this.$refs.input.value = ''
this.$refs.input.blur()
}
}
}"
@click.away="results = []">
<label for="search" class="sr-only">Search parks</label>
<div class="relative">
<input type="search"
name="search"
id="search"
x-ref="input"
class="block w-full rounded-md border-gray-300 bg-white py-3 pl-4 pr-10 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 sm:text-sm"
placeholder="Search parks by name or location..."
hx-get="{% url 'parks:search_parks' %}"
hx-trigger="keyup changed delay:300ms, search"
hx-target="#search-results"
hx-indicator="#search-indicator"
@keydown="onKeyDown($event)"
autocomplete="off"
role="combobox"
aria-expanded="false"
aria-controls="search-results"
aria-autocomplete="list">
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
<div id="search-indicator" class="htmx-indicator">
<svg class="h-5 w-5 text-gray-400 animate-spin" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
</div>
</div>
<div class="max-w-3xl mx-auto relative mb-8">
<label for="search" class="sr-only">Search parks</label>
<input type="search"
name="search"
id="search"
class="block w-full rounded-md border-gray-300 bg-white py-3 pl-4 pr-10 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 sm:text-sm"
placeholder="Search parks by name or location..."
hx-get="{% url 'parks:search_parks' %}"
hx-trigger="input delay:300ms, search"
hx-target="#park-results"
hx-push-url="true"
hx-indicator="#search-indicator"
value="{{ request.GET.search|default:'' }}"
aria-label="Search parks">
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
<div id="search-indicator" class="htmx-indicator">
<svg class="h-5 w-5 text-gray-400 animate-spin" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
</div>
<div id="search-results"
class="mt-2"
role="listbox"
x-init="$watch('$el.children', value => results = Array.from(value))"></div>
</div>
</div>
{# Advanced Filters #}
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">Advanced Filters</h3>
<div class="mt-4">
{% include "search/partials/filter_form.html" with filter=filter %}
</div>
<h3 class="text-lg font-medium leading-6 text-gray-900">Filters</h3>
<form id="filter-form"
hx-get="{% url 'parks:park_list' %}"
hx-target="#park-results"
hx-push-url="true"
hx-trigger="change"
class="mt-4">
{% include "search/components/filter_form.html" with filter=filter %}
</form>
</div>
</div>
</div>
{% endblock %}
{% block results_section_title %}All Parks{% endblock %}
{% block no_results_message %}
<div class="text-center p-8 text-gray-500">
No parks found matching your criteria. Try adjusting your filters or <a href="{% url 'parks:park_create' %}" class="text-blue-600 hover:underline">add a new park</a>.
</div>
{% endblock %}
{% block results_list %}
<div class="bg-white rounded-lg shadow">
{% include "parks/partials/park_list_item.html" with parks=parks %}
<div id="park-results"
class="bg-white rounded-lg shadow"
data-view-mode="{{ view_mode|default:'grid' }}">
{% include "parks/partials/park_list_item.html" with parks=parks view_mode=view_mode|default:'grid' %}
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'parks/js/search.js' %}"></script>
{% endblock %}

View File

@@ -1,58 +0,0 @@
{% load static %}
{% load filter_utils %}
<div class="park-card group relative border rounded-lg p-4 transition-all duration-200 ease-in-out {% if view_mode == 'grid' %}grid-item hover:shadow-lg{% else %}flex items-start space-x-4 hover:bg-gray-50{% endif %}">
<a href="{% url 'parks:park_detail' park.slug %}" class="absolute inset-0 z-0"></a>
<div class="relative z-10">
{% if park.photos.exists %}
<img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}"
class="{% if view_mode == 'grid' %}w-full h-48 object-cover rounded-lg mb-4{% else %}w-24 h-24 object-cover rounded-lg{% endif %}">
{% else %}
<div class="{% if view_mode == 'grid' %}w-full h-48 bg-gray-100 rounded-lg mb-4{% else %}w-24 h-24 bg-gray-100 rounded-lg{% endif %} flex items-center justify-center">
<span class="text-2xl font-medium text-gray-400">{{ park.name|first|upper }}</span>
</div>
{% endif %}
</div>
<div class="{% if view_mode != 'grid' %}flex-1 min-w-0{% endif %}">
<h3 class="text-lg font-semibold truncate">
{{ park.name }}
</h3>
<div class="mt-1 text-sm text-gray-500 truncate">
{% with location=park.location.first %}
{% if location %}
{{ location.city }}{% if location.state %}, {{ location.state }}{% endif %}{% if location.country %}, {{ location.country }}{% endif %}
{% else %}
Location unknown
{% endif %}
{% endwith %}
</div>
<div class="mt-2 flex flex-wrap gap-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ park.get_status_color }}">
{{ park.get_status_display }}
</span>
{% if park.opening_date %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Opened {{ park.opening_date|date:"Y" }}
</span>
{% endif %}
{% if park.ride_count %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ park.ride_count }} rides
</span>
{% endif %}
{% if park.coaster_count %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
{{ park.coaster_count }} coasters
</span>
{% endif %}
</div>
</div>
</div>

View File

@@ -1,5 +1,8 @@
{% load static %}
{% load filter_utils %}
{% if error %}
<div class="p-4">
<div class="p-4" data-testid="park-list-error">
<div class="inline-flex items-center px-4 py-2 rounded-md bg-red-50 text-red-700">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
@@ -8,13 +11,89 @@
</div>
</div>
{% else %}
<div class="{% if view_mode == 'grid' %}grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6{% else %}space-y-6{% endif %}">
{% for park in object_list|default:parks %}
{% include "parks/partials/park_card.html" with park=park view_mode=view_mode %}
{% empty %}
<div class="p-4 text-sm text-gray-500 text-center">
No parks found matching your search.
</div>
{% endfor %}
<div class="{% if view_mode == 'grid' %}grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 p-4{% else %}flex flex-col gap-4 p-4{% endif %}"
data-testid="park-list"
data-view-mode="{{ view_mode|default:'grid' }}">
{% for park in object_list|default:parks %}
<article class="park-card group relative bg-white border rounded-lg transition-all duration-200 ease-in-out hover:shadow-lg {% if view_mode == 'list' %}flex gap-4 p-4{% endif %}"
data-testid="park-card"
data-park-id="{{ park.id }}"
data-view-mode="{{ view_mode|default:'grid' }}">
<a href="{% url 'parks:park_detail' park.slug %}"
class="absolute inset-0 z-0"
aria-label="View details for {{ park.name }}"></a>
<div class="relative z-10 {% if view_mode == 'grid' %}aspect-video{% endif %}">
{% if park.photos.exists %}
<img src="{{ park.photos.first.image.url }}"
alt="Photo of {{ park.name }}"
class="{% if view_mode == 'grid' %}w-full h-full object-cover rounded-t-lg{% else %}w-24 h-24 object-cover rounded-lg flex-shrink-0{% endif %}"
loading="lazy">
{% else %}
<div class="{% if view_mode == 'grid' %}w-full h-full bg-gray-100 rounded-t-lg flex items-center justify-center{% else %}w-24 h-24 bg-gray-100 rounded-lg flex-shrink-0 flex items-center justify-center{% endif %}"
role="img"
aria-label="Park initial letter">
<span class="text-2xl font-medium text-gray-400">{{ park.name|first|upper }}</span>
</div>
{% endif %}
</div>
<div class="{% if view_mode == 'grid' %}p-4{% else %}flex-1 min-w-0{% endif %}">
<h3 class="text-lg font-semibold text-gray-900 truncate group-hover:text-blue-600">
{{ park.name }}
</h3>
<div class="mt-1 text-sm text-gray-500 truncate">
{% with location=park.location.first %}
{% if location %}
{{ location.city }}{% if location.state %}, {{ location.state }}{% endif %}{% if location.country %}, {{ location.country }}{% endif %}
{% else %}
Location unknown
{% endif %}
{% endwith %}
</div>
<div class="mt-2 flex flex-wrap gap-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ park.get_status_color }} status-badge"
data-testid="park-status">
{{ park.get_status_display }}
</span>
{% if park.opening_date %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800"
data-testid="park-opening-date">
Opened {{ park.opening_date|date:"Y" }}
</span>
{% endif %}
{% if park.current_ride_count %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
data-testid="park-ride-count">
{{ park.current_ride_count }} ride{{ park.current_ride_count|pluralize }}
</span>
{% endif %}
{% if park.current_coaster_count %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"
data-testid="park-coaster-count">
{{ park.current_coaster_count }} coaster{{ park.current_coaster_count|pluralize }}
</span>
{% endif %}
</div>
</div>
</article>
{% empty %}
<div class="{% if view_mode == 'grid' %}col-span-full{% endif %} p-4 text-sm text-gray-500 text-center" data-testid="no-parks-found">
{% if search_query %}
No parks found matching "{{ search_query }}". Try adjusting your search terms.
{% else %}
No parks found matching your criteria. Try adjusting your filters.
{% endif %}
{% if user.is_authenticated %}
You can also <a href="{% url 'parks:park_create' %}" class="text-blue-600 hover:underline">add a new park</a>.
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}