mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:11:08 -05:00
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:
@@ -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
|
|
||||||
@@ -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 %}
|
||||||
58
parks/templates/parks/partials/park_card.html
Normal file
58
parks/templates/parks/partials/park_card.html
Normal 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>
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user