Refactor park listing templates: implement grid and list view modes, enhance search results rendering, and improve error handling in search functionality

This commit is contained in:
pacnpal
2025-02-13 12:19:23 -05:00
parent bba707fa98
commit 9d6f6dab2c
5 changed files with 112 additions and 227 deletions

View File

@@ -1,153 +1,28 @@
# Active Context # Active Context - Park View Modularization
## Current Project State **Objective:** Refactor parks view to use reusable card component and implement grid/list view toggle
### Active Components **Current Implementation Analysis:**
- Django backend with core apps - Park cards rendered via `park_list_item.html` partial
- accounts - Existing layout uses flex-based list structure
- analytics - Search functionality uses HTMX for dynamic updates
- companies
- core
- designers
- email_service
- history_tracking
- location
- media
- moderation
- parks
- reviews
- rides
### Implementation Status **Planned Changes:**
1. Backend Framework 1. **Create `park_card.html` Partial**
- ✅ Django setup - Extract card markup from `park_list_item.html`
- ✅ Database models - Add responsive grid/list view classes
- ✅ Authentication system - Include view mode toggle state
- ✅ Admin interface
2. Frontend Integration 2. **View Toggle Implementation**
- ✅ HTMX integration - Add grid/list toggle UI with HTMX
- ✅ AlpineJS setup - Store view preference in cookie/localStorage
- ✅ Tailwind CSS configuration - Update CSS for grid (grid-cols) vs list (flex) layouts
3. Core Features 3. **Backend Updates**
- ✅ User authentication - Add view_mode parameter to park list view
- ✅ Park management - Modify context processor to handle layout preference
- ✅ Ride tracking
- ✅ Review system
- ✅ Location services
- ✅ Media handling
## Current Focus Areas **Next Steps:**
- Implement card partial with responsive classes
### Active Development - Create view toggle component
1. Content Management - Update HTMX handlers to preserve view mode
- Moderation workflow refinement
- Content quality metrics
- User contribution tracking
2. User Experience
- Frontend performance optimization
- UI/UX improvements
- Responsive design enhancements
3. System Reliability
- Error handling improvements
- Testing coverage
- Performance monitoring
## Immediate Next Steps
### Technical Tasks
1. Testing
- [ ] Increase test coverage
- [ ] Implement integration tests
- [ ] Add performance tests
2. Documentation
- [ ] Complete API documentation
- [ ] Update setup guides
- [ ] Document common workflows
3. Performance
- [ ] Optimize database queries
- [ ] Implement caching strategy
- [ ] Improve asset loading
### Feature Development
1. Content Quality
- [ ] Enhanced moderation tools
- [ ] Automated content checks
- [ ] Media optimization
2. User Features
- [ ] Profile enhancements
- [ ] Contribution tracking
- [ ] Notification system
## Known Issues
### Backend
1. Performance
- Query optimization needed for large datasets
- Caching implementation incomplete
2. Technical Debt
- Some views need refactoring
- Test coverage gaps
- Documentation updates needed
### Frontend
1. UI/UX
- Mobile responsiveness improvements
- Loading state refinements
- Error feedback enhancements
2. Technical
- JavaScript optimization needed
- Asset loading optimization
- Form validation improvements
## Recent Changes
### Last Update: 2025-02-12
- Integrated parks app with site-wide search system
* Added comprehensive filter configuration
* Implemented error handling
* Created both full and quick search interfaces
* See `features/search/park-search.md` for details
### Previous Update: 2025-02-06
1. Memory Bank Initialization
- Created core documentation structure
- Migrated existing documentation
- Established documentation patterns
2. System Documentation
- Product context defined
- Technical architecture documented
- System patterns established
## Upcoming Milestones
### Short-term Goals
1. Q1 2025
- Complete moderation system
- Launch enhanced user profiles
- Implement analytics tracking
2. Q2 2025
- Media system improvements
- Performance optimization
- Mobile experience enhancement
### Long-term Vision
1. Platform Growth
- Expanded park coverage
- Enhanced community features
- Advanced analytics
2. Technical Evolution
- Architecture scalability
- Feature extensibility
- Performance optimization

View File

@@ -20,8 +20,16 @@
{% endblock %} {% endblock %}
{% block list_header %} {% 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>
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">Parks</h1> <h1 class="text-2xl font-bold text-gray-900 mr-4">Parks</h1>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a href="{% url 'parks:park_create' %}" class="btn btn-primary"> <a href="{% url 'parks:park_create' %}" class="btn btn-primary">
Add Park Add Park
@@ -122,10 +130,8 @@ Browse and filter amusement parks, theme parks, and water parks from around the
</div> </div>
{% endblock %} {% endblock %}
{% block results_section %} {% block results_list %}
<div class="space-y-6"> <div class="bg-white rounded-lg shadow">
{% for park in parks %} {% include "parks/partials/park_list_item.html" with parks=parks %}
{% include "parks/partials/park_list_item.html" with park=park %}
{% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,58 @@
{% 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

@@ -8,67 +8,9 @@
</div> </div>
</div> </div>
{% else %} {% else %}
<div class="divide-y"> <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 %} {% for park in object_list|default:parks %}
<div role="option" {% include "parks/partials/park_card.html" with park=park view_mode=view_mode %}
id="result-{{ forloop.counter0 }}"
:class="{ 'bg-gray-50 dark:bg-gray-800': selectedIndex === {{ forloop.counter0 }} }"
class="p-4 flex items-start space-x-4">
{% if park.photos.exists %}
<img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}"
class="w-24 h-24 object-cover rounded-lg">
{% else %}
<div class="w-24 h-24 bg-gray-100 rounded-lg flex items-center justify-center">
<span class="text-2xl font-medium text-gray-400">{{ park.name|first|upper }}</span>
</div>
{% endif %}
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold truncate">
<a href="{% url 'parks:park_detail' park.slug %}"
x-ref="result-{{ forloop.counter0 }}"
:class="{ 'bg-gray-50 dark:bg-gray-800': selectedIndex === {{ forloop.counter0 }} }"
class="hover:text-blue-600 block w-full py-1 px-2 -mx-2 rounded">
{{ park.name }}
</a>
</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>
{% empty %} {% empty %}
<div class="p-4 text-sm text-gray-500 text-center"> <div class="p-4 text-sm text-gray-500 text-center">
No parks found matching your search. No parks found matching your search.

View File

@@ -55,7 +55,7 @@ def search_parks(request: HttpRequest) -> HttpResponse:
try: try:
search_query = request.GET.get('search', '').strip() search_query = request.GET.get('search', '').strip()
if not search_query: if not search_query:
return HttpResponse('') # Return empty response for empty query return HttpResponse('') # Keep empty string for clearing search results
queryset = ( queryset = (
Park.objects.select_related('owner') Park.objects.select_related('owner')
@@ -74,19 +74,15 @@ def search_parks(request: HttpRequest) -> HttpResponse:
parks = park_filter.qs[:8] # Limit to 8 suggestions parks = park_filter.qs[:8] # Limit to 8 suggestions
if not parks: response = render(request, "parks/park_list.html", {
return HttpResponse( "parks": parks
'<div class="p-4 text-sm text-gray-500">No parks found matching your search.</div>'
)
response = render(request, "parks/partials/park_list_item.html", {
"object_list": parks # Use object_list to match template's for loop
}) })
response['HX-Trigger'] = 'searchComplete' response['HX-Trigger'] = 'searchComplete'
return response return response
except Exception as e: except Exception as e:
response = render(request, "parks/partials/park_list_item.html", { response = render(request, "parks/park_list.html", {
"parks": [],
"error": f"Error performing search: {str(e)}" "error": f"Error performing search: {str(e)}"
}) })
response['HX-Trigger'] = 'searchError' response['HX-Trigger'] = 'searchError'
@@ -184,6 +180,12 @@ class ParkListView(HTMXFilterableMixin, ListView):
filter_class = ParkFilter filter_class = ParkFilter
paginate_by = 20 paginate_by = 20
def get_template_names(self) -> list[str]:
"""Override to use same template for HTMX and regular requests"""
if self.request.htmx:
return ["parks/partials/park_list_item.html"]
return [self.template_name]
def get_queryset(self) -> QuerySet[Park]: def get_queryset(self) -> QuerySet[Park]:
try: try:
return ( return (
@@ -209,14 +211,16 @@ class ParkListView(HTMXFilterableMixin, ListView):
def get_context_data(self, **kwargs: Any) -> dict[str, Any]: def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
try: try:
return super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['results_template'] = "parks/partials/park_list_item.html"
return context
except Exception as e: except Exception as e:
messages.error(self.request, f"Error applying filters: {str(e)}") messages.error(self.request, f"Error applying filters: {str(e)}")
context = { return {
"filter": self.filterset, "filter": self.filterset,
"error": "Unable to apply filters. Please try adjusting your criteria." "error": "Unable to apply filters. Please try adjusting your criteria.",
"results_template": "parks/partials/park_list_item.html"
} }
return context
class ParkDetailView( class ParkDetailView(