Enhance park search functionality: update view mode handling and improve park list item layout

This commit is contained in:
pacnpal
2025-02-21 18:52:01 -05:00
parent 518fcbee22
commit d7951756dc
3 changed files with 92 additions and 71 deletions

View File

@@ -52,7 +52,7 @@
id="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" 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..." placeholder="Search parks by name or location..."
hx-get="{% url 'parks:search_parks' %}" hx-get="{% url 'parks:search_parks' %}?view_mode={{ view_mode|default:'grid' }}"
hx-trigger="input delay:300ms, search" hx-trigger="input delay:300ms, search"
hx-target="#park-results" hx-target="#park-results"
hx-push-url="true" hx-push-url="true"

View File

@@ -11,80 +11,33 @@
</div> </div>
</div> </div>
{% else %} {% else %}
<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 %}" <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
data-testid="park-list"
data-view-mode="{{ view_mode|default:'grid' }}">
{% for park in object_list|default:parks %} {% 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 %}" <div class="overflow-hidden transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
data-testid="park-card" <div class="p-4">
data-park-id="{{ park.id }}" <h2 class="mb-2 text-xl font-bold">
data-view-mode="{{ view_mode|default:'grid' }}"> <a href="{% url 'parks:park_detail' park.slug %}" class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
<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 }} {{ park.name }}
</h3> </a>
</h2>
<div class="mt-1 text-sm text-gray-500 truncate"> <div class="flex flex-wrap gap-2">
{% with location=park.location.first %} <span class="status-badge status-{{ park.status|lower }}">
{% 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 }} {{ park.get_status_display }}
</span> </span>
</div>
{% if park.opening_date %} {% if park.owner %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800" <div class="mt-4 text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
data-testid="park-opening-date"> <a href="{% url 'companies:company_detail' park.owner.slug %}">
Opened {{ park.opening_date|date:"Y" }} {{ park.owner.name }}
</span> </a>
{% endif %} </div>
{% 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 %} {% endif %}
</div> </div>
</div> </div>
</article>
{% empty %} {% 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"> <div class="col-span-full p-4 text-sm text-gray-500 text-center" data-testid="no-parks-found">
{% if search_query %} {% if search_query %}
No parks found matching "{{ search_query }}". Try adjusting your search terms. No parks found matching "{{ search_query }}". Try adjusting your search terms.
{% else %} {% else %}

View File

@@ -1,6 +1,8 @@
import requests
from decimal import Decimal, ROUND_DOWN from decimal import Decimal, ROUND_DOWN
from typing import Any, Optional, cast, Literal from typing import Any, Optional, cast, Literal
from django.views.generic import DetailView, ListView, CreateView, UpdateView from django.views.generic import DetailView, ListView, CreateView, UpdateView
from decimal import InvalidOperation
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.db.models import Q, Count, QuerySet from django.db.models import Q, Count, QuerySet
@@ -23,6 +25,70 @@ from search.mixins import HTMXFilterableMixin
ViewMode = Literal["grid", "list"] ViewMode = Literal["grid", "list"]
def normalize_osm_result(result: dict) -> dict:
"""Normalize OpenStreetMap result to a consistent format with enhanced address details"""
from .location_utils import get_english_name, normalize_coordinate
# Get address details
address = result.get('address', {})
# Normalize coordinates
lat = normalize_coordinate(float(result.get('lat')), 9, 6)
lon = normalize_coordinate(float(result.get('lon')), 10, 6)
# Get English names where possible
name = ''
if 'namedetails' in result:
name = get_english_name(result['namedetails'])
# Build street address from available components
street_parts = []
if address.get('house_number'):
street_parts.append(address['house_number'])
if address.get('road') or address.get('street'):
street_parts.append(address.get('road') or address.get('street'))
elif address.get('pedestrian'):
street_parts.append(address['pedestrian'])
elif address.get('footway'):
street_parts.append(address['footway'])
# Handle additional address components
suburb = address.get('suburb', '')
district = address.get('district', '')
neighborhood = address.get('neighbourhood', '')
# Build city from available components
city = (address.get('city') or
address.get('town') or
address.get('village') or
address.get('municipality') or
'')
# Get detailed state/region information
state = (address.get('state') or
address.get('province') or
address.get('region') or
'')
# Get postal code with fallbacks
postal_code = (address.get('postcode') or
address.get('postal_code') or
'')
return {
'display_name': name or result.get('display_name', ''),
'lat': lat,
'lon': lon,
'street': ' '.join(street_parts).strip(),
'suburb': suburb,
'district': district,
'neighborhood': neighborhood,
'city': city,
'state': state,
'country': address.get('country', ''),
'postal_code': postal_code,
}
def get_view_mode(request: HttpRequest) -> ViewMode: def get_view_mode(request: HttpRequest) -> ViewMode:
"""Get the current view mode from request, defaulting to grid""" """Get the current view mode from request, defaulting to grid"""
view_mode = request.GET.get('view_mode', 'grid') view_mode = request.GET.get('view_mode', 'grid')
@@ -202,6 +268,8 @@ def search_parks(request: HttpRequest) -> HttpResponse:
if not search_query: if not search_query:
return HttpResponse('') return HttpResponse('')
# Get current view mode from request
current_view_mode = request.GET.get('view_mode', 'grid')
park_filter = ParkFilter({ park_filter = ParkFilter({
'search': search_query 'search': search_query
}, queryset=get_base_park_queryset()) }, queryset=get_base_park_queryset())
@@ -215,7 +283,7 @@ def search_parks(request: HttpRequest) -> HttpResponse:
"parks/partials/park_list_item.html", "parks/partials/park_list_item.html",
{ {
"parks": parks, "parks": parks,
"view_mode": get_view_mode(request), "view_mode": current_view_mode,
"search_query": search_query, "search_query": search_query,
"is_search": True "is_search": True
} }