mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 12:11:13 -05:00
remove backend
This commit is contained in:
310
templates/core/search/filters.html
Normal file
310
templates/core/search/filters.html
Normal file
@@ -0,0 +1,310 @@
|
||||
{# Modern Filter Interface - timestamp: 2025-08-29 #}
|
||||
{% load static %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<div class="space-y-6">
|
||||
{# Search Section #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
Search
|
||||
</h3>
|
||||
|
||||
<form hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#results-container"
|
||||
hx-trigger="input changed delay:500ms, submit"
|
||||
hx-indicator="#loading-indicator"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
class="space-y-4">
|
||||
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
name="search"
|
||||
value="{{ request.GET.search|default:'' }}"
|
||||
placeholder="Search parks by name, location..."
|
||||
class="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Filters Section #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.414A1 1 0 013 6.707V4z"/>
|
||||
</svg>
|
||||
Filters
|
||||
</h3>
|
||||
|
||||
<form id="filter-form"
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#results-container"
|
||||
hx-trigger="change delay:300ms, submit"
|
||||
hx-indicator="#loading-indicator"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
class="space-y-6">
|
||||
|
||||
{# Preserve search term #}
|
||||
{% if request.GET.search %}
|
||||
<input type="hidden" name="search" value="{{ request.GET.search }}">
|
||||
{% endif %}
|
||||
|
||||
{# Status Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Status
|
||||
</label>
|
||||
<select name="status"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
<option value="">All Status</option>
|
||||
<option value="OPERATING" {% if request.GET.status == 'OPERATING' %}selected{% endif %}>Operating</option>
|
||||
<option value="CLOSED_TEMP" {% if request.GET.status == 'CLOSED_TEMP' %}selected{% endif %}>Temporarily Closed</option>
|
||||
<option value="CLOSED_PERM" {% if request.GET.status == 'CLOSED_PERM' %}selected{% endif %}>Permanently Closed</option>
|
||||
<option value="UNDER_CONSTRUCTION" {% if request.GET.status == 'UNDER_CONSTRUCTION' %}selected{% endif %}>Under Construction</option>
|
||||
<option value="DEMOLISHED" {% if request.GET.status == 'DEMOLISHED' %}selected{% endif %}>Demolished</option>
|
||||
<option value="RELOCATED" {% if request.GET.status == 'RELOCATED' %}selected{% endif %}>Relocated</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Operator Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Operator
|
||||
</label>
|
||||
<select name="operator"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
<option value="">All Operators</option>
|
||||
<option value="cedar-fair" {% if request.GET.operator == 'cedar-fair' %}selected{% endif %}>Cedar Fair</option>
|
||||
<option value="six-flags" {% if request.GET.operator == 'six-flags' %}selected{% endif %}>Six Flags</option>
|
||||
<option value="disney" {% if request.GET.operator == 'disney' %}selected{% endif %}>Disney</option>
|
||||
<option value="universal" {% if request.GET.operator == 'universal' %}selected{% endif %}>Universal</option>
|
||||
<option value="busch-gardens" {% if request.GET.operator == 'busch-gardens' %}selected{% endif %}>Busch Gardens</option>
|
||||
<option value="knott's" {% if request.GET.operator == 'knotts' %}selected{% endif %}>Knott's</option>
|
||||
<option value="herschend" {% if request.GET.operator == 'herschend' %}selected{% endif %}>Herschend</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Rating Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Minimum Rating
|
||||
</label>
|
||||
<select name="min_rating"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
<option value="">Any Rating</option>
|
||||
<option value="1" {% if request.GET.min_rating == '1' %}selected{% endif %}>1+ Stars</option>
|
||||
<option value="2" {% if request.GET.min_rating == '2' %}selected{% endif %}>2+ Stars</option>
|
||||
<option value="3" {% if request.GET.min_rating == '3' %}selected{% endif %}>3+ Stars</option>
|
||||
<option value="4" {% if request.GET.min_rating == '4' %}selected{% endif %}>4+ Stars</option>
|
||||
<option value="5" {% if request.GET.min_rating == '5' %}selected{% endif %}>5+ Stars</option>
|
||||
<option value="6" {% if request.GET.min_rating == '6' %}selected{% endif %}>6+ Stars</option>
|
||||
<option value="7" {% if request.GET.min_rating == '7' %}selected{% endif %}>7+ Stars</option>
|
||||
<option value="8" {% if request.GET.min_rating == '8' %}selected{% endif %}>8+ Stars</option>
|
||||
<option value="9" {% if request.GET.min_rating == '9' %}selected{% endif %}>9+ Stars</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Ride Count Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Minimum Rides
|
||||
</label>
|
||||
<input type="number"
|
||||
name="min_rides"
|
||||
value="{{ request.GET.min_rides|default:'' }}"
|
||||
min="0"
|
||||
placeholder="Any number"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
</div>
|
||||
|
||||
{# Coaster Count Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Minimum Coasters
|
||||
</label>
|
||||
<input type="number"
|
||||
name="min_coasters"
|
||||
value="{{ request.GET.min_coasters|default:'' }}"
|
||||
min="0"
|
||||
placeholder="Any number"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
</div>
|
||||
|
||||
{# Opening Year Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Opening Year
|
||||
</label>
|
||||
<input type="number"
|
||||
name="opening_year"
|
||||
value="{{ request.GET.opening_year|default:'' }}"
|
||||
min="1800"
|
||||
max="2030"
|
||||
placeholder="Any year"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
</div>
|
||||
|
||||
{# Size Filter #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Minimum Size (acres)
|
||||
</label>
|
||||
<input type="number"
|
||||
name="min_size"
|
||||
value="{{ request.GET.min_size|default:'' }}"
|
||||
min="0"
|
||||
step="0.1"
|
||||
placeholder="Any size"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
</div>
|
||||
|
||||
{# Special Features #}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
Special Features
|
||||
</label>
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="has_roller_coasters"
|
||||
value="true"
|
||||
{% if request.GET.has_roller_coasters %}checked{% endif %}
|
||||
class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 focus:ring-2">
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Has Roller Coasters</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="has_water_rides"
|
||||
value="true"
|
||||
{% if request.GET.has_water_rides %}checked{% endif %}
|
||||
class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 focus:ring-2">
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Has Water Rides</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="family_friendly"
|
||||
value="true"
|
||||
{% if request.GET.family_friendly %}checked{% endif %}
|
||||
class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 focus:ring-2">
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Family Friendly</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Sort & View Options #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
|
||||
</svg>
|
||||
Sort By
|
||||
</h3>
|
||||
|
||||
<form hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#results-container"
|
||||
hx-trigger="change"
|
||||
hx-indicator="#loading-indicator"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true">
|
||||
|
||||
{# Preserve all current filters #}
|
||||
{% for key, value in request.GET.items %}
|
||||
{% if key != 'sort' and key != 'view_mode' %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<select name="sort"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
<option value="name" {% if request.GET.sort == 'name' or not request.GET.sort %}selected{% endif %}>Name A-Z</option>
|
||||
<option value="-name" {% if request.GET.sort == '-name' %}selected{% endif %}>Name Z-A</option>
|
||||
<option value="-average_rating" {% if request.GET.sort == '-average_rating' %}selected{% endif %}>Highest Rated</option>
|
||||
<option value="average_rating" {% if request.GET.sort == 'average_rating' %}selected{% endif %}>Lowest Rated</option>
|
||||
<option value="-opening_date" {% if request.GET.sort == '-opening_date' %}selected{% endif %}>Newest</option>
|
||||
<option value="opening_date" {% if request.GET.sort == 'opening_date' %}selected{% endif %}>Oldest</option>
|
||||
<option value="-ride_count" {% if request.GET.sort == '-ride_count' %}selected{% endif %}>Most Rides</option>
|
||||
<option value="-coaster_count" {% if request.GET.sort == '-coaster_count' %}selected{% endif %}>Most Coasters</option>
|
||||
<option value="-size" {% if request.GET.sort == '-size' %}selected{% endif %}>Largest</option>
|
||||
<option value="size" {% if request.GET.sort == 'size' %}selected{% endif %}>Smallest</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Clear Filters Button #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="p-4">
|
||||
<button type="button"
|
||||
onclick="clearAllFilters()"
|
||||
class="w-full px-4 py-3 bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 text-white font-medium rounded-lg transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Clear All Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Loading Indicator #}
|
||||
<div id="loading-indicator" class="htmx-indicator fixed top-4 right-4 z-50">
|
||||
<div class="bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function clearAllFilters() {
|
||||
// Navigate to the base URL without any query parameters
|
||||
window.location.href = "{% url 'parks:park_list' %}";
|
||||
}
|
||||
|
||||
// Enhanced form handling
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Auto-submit forms on change with debouncing
|
||||
const forms = document.querySelectorAll('#filter-form, form[hx-trigger*="change"]');
|
||||
forms.forEach(form => {
|
||||
let timeout;
|
||||
form.addEventListener('change', function(e) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
htmx.trigger(form, 'submit');
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle search input with longer delay
|
||||
const searchInput = document.querySelector('input[name="search"]');
|
||||
if (searchInput) {
|
||||
let searchTimeout;
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
htmx.trigger(e.target.closest('form'), 'submit');
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
190
templates/core/search/layouts/filtered_list.html
Normal file
190
templates/core/search/layouts/filtered_list.html
Normal file
@@ -0,0 +1,190 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Parks - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
|
||||
{# Enhanced Container with Modern Responsive Design #}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-6 lg:py-8">
|
||||
<div class="grid grid-cols-1 xl:grid-cols-4 gap-6 lg:gap-8 max-w-7xl mx-auto">
|
||||
{# Enhanced Filters Sidebar with Sticky Positioning #}
|
||||
<aside class="xl:col-span-1 order-2 xl:order-1">
|
||||
<div class="sticky top-4 z-30 space-y-6">
|
||||
{# Desktop Filter Content Container #}
|
||||
<div id="desktop-filter-panel" class="hidden xl:block">
|
||||
<div class="xl:max-h-[calc(100vh-2rem)] xl:overflow-y-auto xl:scrollbar-thin xl:scrollbar-thumb-gray-300 dark:xl:scrollbar-thumb-gray-600 xl:scrollbar-track-transparent">
|
||||
{% include "core/search/filters.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{# Enhanced Results Section with Better Responsiveness #}
|
||||
<main class="xl:col-span-3 order-1 xl:order-2">
|
||||
<div id="results-container" class="space-y-6">
|
||||
{# Mobile Filter Toggle and Panel - Moved to top for better UX #}
|
||||
<div class="xl:hidden">
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg">
|
||||
<button type="button"
|
||||
id="mobile-filter-toggle"
|
||||
class="w-full flex items-center justify-between p-4 text-left font-medium text-gray-900 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-700/50 rounded-xl transition-all duration-200"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-filter-panel">
|
||||
<span class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.414A1 1 0 013 6.707V4z"/>
|
||||
</svg>
|
||||
Filters & Search
|
||||
</span>
|
||||
<svg class="w-5 h-5 text-gray-400 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{# Mobile Filter Panel - Adjacent to toggle for proper JavaScript functionality #}
|
||||
<div id="mobile-filter-panel" class="hidden border-t border-gray-200/50 dark:border-gray-700/50">
|
||||
<div class="p-4">
|
||||
{% block filter_section %}
|
||||
<!-- Mobile filter content -->
|
||||
{% include "core/search/filters.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Enhanced Header Section #}
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg">
|
||||
<div class="p-4 lg:p-6">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<h2 class="text-xl lg:text-2xl font-bold bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent">
|
||||
Parks
|
||||
<span class="text-sm lg:text-base font-normal text-gray-500 dark:text-gray-400">
|
||||
({{ page_obj.paginator.count }} found)
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{% block list_actions %}
|
||||
{# Custom actions can be added here by extending views #}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Enhanced Results Content #}
|
||||
{% block results_list %}
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg overflow-hidden">
|
||||
<div class="p-4 lg:p-6">
|
||||
{% include results_template|default:"search/partials/generic_results.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# Enhanced Pagination #}
|
||||
{% if is_paginated %}
|
||||
<div class="flex justify-center">
|
||||
<div class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg px-6 py-4">
|
||||
{% include "search/components/pagination.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Enhanced Mobile Filter Panel JavaScript #}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mobileToggle = document.getElementById('mobile-filter-toggle');
|
||||
const mobilePanel = document.getElementById('mobile-filter-panel');
|
||||
|
||||
if (mobileToggle && mobilePanel) {
|
||||
mobileToggle.addEventListener('click', function() {
|
||||
const isExpanded = mobileToggle.getAttribute('aria-expanded') === 'true';
|
||||
const newState = !isExpanded;
|
||||
|
||||
// Update aria-expanded
|
||||
mobileToggle.setAttribute('aria-expanded', newState.toString());
|
||||
|
||||
// Toggle panel visibility with animation
|
||||
if (newState) {
|
||||
mobilePanel.classList.remove('hidden');
|
||||
mobilePanel.style.maxHeight = '0px';
|
||||
mobilePanel.style.overflow = 'hidden';
|
||||
mobilePanel.style.transition = 'max-height 0.3s ease-out';
|
||||
|
||||
// Trigger reflow
|
||||
mobilePanel.offsetHeight;
|
||||
|
||||
// Expand
|
||||
mobilePanel.style.maxHeight = mobilePanel.scrollHeight + 'px';
|
||||
|
||||
// Clean up after animation
|
||||
setTimeout(() => {
|
||||
mobilePanel.style.maxHeight = '';
|
||||
mobilePanel.style.overflow = '';
|
||||
mobilePanel.style.transition = '';
|
||||
}, 300);
|
||||
} else {
|
||||
mobilePanel.style.maxHeight = mobilePanel.scrollHeight + 'px';
|
||||
mobilePanel.style.overflow = 'hidden';
|
||||
mobilePanel.style.transition = 'max-height 0.3s ease-in';
|
||||
|
||||
// Trigger reflow
|
||||
mobilePanel.offsetHeight;
|
||||
|
||||
// Collapse
|
||||
mobilePanel.style.maxHeight = '0px';
|
||||
|
||||
// Hide after animation
|
||||
setTimeout(() => {
|
||||
mobilePanel.classList.add('hidden');
|
||||
mobilePanel.style.maxHeight = '';
|
||||
mobilePanel.style.overflow = '';
|
||||
mobilePanel.style.transition = '';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Rotate arrow icon
|
||||
const arrow = mobileToggle.querySelector('svg:last-child');
|
||||
if (arrow) {
|
||||
arrow.style.transform = newState ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||
}
|
||||
});
|
||||
|
||||
// Close on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && mobileToggle.getAttribute('aria-expanded') === 'true') {
|
||||
mobileToggle.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Close when clicking outside on mobile
|
||||
document.addEventListener('click', function(e) {
|
||||
if (window.innerWidth < 1280 && // xl breakpoint
|
||||
!mobileToggle.contains(e.target) &&
|
||||
!mobilePanel.contains(e.target) &&
|
||||
mobileToggle.getAttribute('aria-expanded') === 'true') {
|
||||
mobileToggle.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
// Handle browser back/forward with HTMX
|
||||
document.body.addEventListener('htmx:beforeOnLoad', function(evt) {
|
||||
if (evt.detail.requestConfig.verb === "get") {
|
||||
history.replaceState(null, '', evt.detail.requestConfig.path);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
332
templates/core/search/location_results.html
Normal file
332
templates/core/search/location_results.html
Normal file
@@ -0,0 +1,332 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Location Search - ThrillWiki{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.search-result-card {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.search-result-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.distance-badge {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.content-type-badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Search Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Location Search Results
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
Found {{ total_results }} result{{ total_results|pluralize }} across parks, rides, and companies
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-8">
|
||||
<!-- Enhanced Search Filters -->
|
||||
<div class="lg:w-1/4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 sticky top-4">
|
||||
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">Search Filters</h2>
|
||||
|
||||
<form hx-get="{% url 'search:location_search' %}"
|
||||
hx-target="#search-results"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#search-loading"
|
||||
class="space-y-4">
|
||||
|
||||
<!-- Text Search -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ search_form.q.label }}
|
||||
</label>
|
||||
{{ search_form.q }}
|
||||
</div>
|
||||
|
||||
<!-- Location Search -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ search_form.location.label }}
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
{{ search_form.location }}
|
||||
{{ search_form.lat }}
|
||||
{{ search_form.lng }}
|
||||
<div class="flex gap-2">
|
||||
<button type="button"
|
||||
id="use-my-location"
|
||||
class="flex-1 px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300">
|
||||
📍 Use My Location
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Radius -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ search_form.radius_km.label }}
|
||||
</label>
|
||||
{{ search_form.radius_km }}
|
||||
</div>
|
||||
|
||||
<!-- Content Types -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Search In
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
{{ search_form.search_parks }}
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{ search_form.search_parks.label }}</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
{{ search_form.search_rides }}
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{ search_form.search_rides.label }}</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
{{ search_form.search_companies }}
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{ search_form.search_companies.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Geographic Filters -->
|
||||
<div class="border-t pt-4">
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Geographic Filters</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
{{ search_form.country }}
|
||||
{{ search_form.state }}
|
||||
{{ search_form.city }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
🔍 Search
|
||||
</button>
|
||||
|
||||
{% if request.GET %}
|
||||
<a href="{% url 'search:location_search' %}"
|
||||
class="block w-full text-center bg-gray-100 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-200 transition-colors dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600">
|
||||
Clear Filters
|
||||
</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section -->
|
||||
<div class="lg:w-3/4" id="search-results">
|
||||
<!-- Loading indicator -->
|
||||
<div id="search-loading" class="hidden">
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if results %}
|
||||
<!-- Results Summary -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow mb-6 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ total_results }} Result{{ total_results|pluralize }} Found
|
||||
</h2>
|
||||
{% if has_location_filter %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Sorted by distance from your location
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="flex gap-4 text-sm">
|
||||
{% if grouped_results.parks %}
|
||||
<span class="bg-green-100 text-green-800 px-2 py-1 rounded dark:bg-green-900 dark:text-green-300">
|
||||
{{ grouped_results.parks|length }} Park{{ grouped_results.parks|length|pluralize }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if grouped_results.rides %}
|
||||
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded dark:bg-blue-900 dark:text-blue-300">
|
||||
{{ grouped_results.rides|length }} Ride{{ grouped_results.rides|length|pluralize }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if grouped_results.companies %}
|
||||
<span class="bg-purple-100 text-purple-800 px-2 py-1 rounded dark:bg-purple-900 dark:text-purple-300">
|
||||
{{ grouped_results.companies|length }} Compan{{ grouped_results.companies|length|pluralize:"y,ies" }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Results -->
|
||||
<div class="space-y-4">
|
||||
{% for result in results %}
|
||||
<div class="search-result-card bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<!-- Header with type badge -->
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="content-type-badge px-2 py-1 rounded-full text-xs
|
||||
{% if result.content_type == 'park' %}bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300
|
||||
{% elif result.content_type == 'ride' %}bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300
|
||||
{% else %}bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300{% endif %}">
|
||||
{{ result.content_type|title }}
|
||||
</span>
|
||||
|
||||
{% if result.distance_km %}
|
||||
<span class="distance-badge text-white px-2 py-1 rounded-full text-xs">
|
||||
{{ result.distance_km|floatformat:1 }} km away
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{% if result.url %}
|
||||
<a href="{{ result.url }}" class="hover:text-blue-600 dark:hover:text-blue-400">
|
||||
{{ result.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ result.name }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
<!-- Description -->
|
||||
{% if result.description %}
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-3 line-clamp-2">
|
||||
{{ result.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Location Info -->
|
||||
{% if result.city or result.address %}
|
||||
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{% if result.address %}
|
||||
{{ result.address }}
|
||||
{% else %}
|
||||
{{ result.city }}{% if result.state %}, {{ result.state }}{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tags and Status -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if result.status %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
|
||||
{{ result.status }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if result.rating %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300">
|
||||
★ {{ result.rating|floatformat:1 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% for tag in result.tags %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
|
||||
{{ tag|title }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map link -->
|
||||
{% if result.latitude and result.longitude %}
|
||||
<div class="ml-4">
|
||||
<a href="{% url 'maps:universal_map' %}?lat={{ result.latitude }}&lng={{ result.longitude }}&zoom=15"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-600">
|
||||
🗺️ View on Map
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- No Results -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-8 text-center">
|
||||
<div class="text-gray-400 mb-4">
|
||||
<svg class="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No results found</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Try adjusting your search criteria or expanding your search radius.
|
||||
</p>
|
||||
<div class="space-y-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<p>• Try broader search terms</p>
|
||||
<p>• Increase the search radius</p>
|
||||
<p>• Check spelling and try different keywords</p>
|
||||
<p>• Remove some filters to see more results</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Geolocation support
|
||||
const useLocationBtn = document.getElementById('use-my-location');
|
||||
const latInput = document.getElementById('lat-input');
|
||||
const lngInput = document.getElementById('lng-input');
|
||||
const locationInput = document.getElementById('location-input');
|
||||
|
||||
if (useLocationBtn && 'geolocation' in navigator) {
|
||||
useLocationBtn.addEventListener('click', function() {
|
||||
this.textContent = '📍 Getting location...';
|
||||
this.disabled = true;
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
latInput.value = position.coords.latitude;
|
||||
lngInput.value = position.coords.longitude;
|
||||
locationInput.value = `${position.coords.latitude}, ${position.coords.longitude}`;
|
||||
useLocationBtn.textContent = '✅ Location set';
|
||||
setTimeout(() => {
|
||||
useLocationBtn.textContent = '📍 Use My Location';
|
||||
useLocationBtn.disabled = false;
|
||||
}, 2000);
|
||||
},
|
||||
function(error) {
|
||||
useLocationBtn.textContent = '❌ Location failed';
|
||||
console.error('Geolocation error:', error);
|
||||
setTimeout(() => {
|
||||
useLocationBtn.textContent = '📍 Use My Location';
|
||||
useLocationBtn.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
);
|
||||
});
|
||||
} else if (useLocationBtn) {
|
||||
useLocationBtn.style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/location-search.js' %}"></script>
|
||||
{% endblock %}
|
||||
134
templates/core/search/partials/generic_results.html
Normal file
134
templates/core/search/partials/generic_results.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<div class="divide-y">
|
||||
{% for object in object_list %}
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold">
|
||||
<a href="{{ object.get_absolute_url }}" class="hover:text-blue-600">
|
||||
{{ object }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{% if object.description %}
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
{{ object.description|truncatewords:30 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% block object_metadata %}
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{% if object.created_at %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Added {{ object.created_at|date }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if object.average_rating %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
{{ object.average_rating }} ★
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if object.location.exists %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{{ object.location.first.get_formatted_address }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="p-8 text-center text-gray-500">
|
||||
<p>No {{ view.model|model_name_plural }} found matching your criteria.</p>
|
||||
{% if applied_filters %}
|
||||
<p class="mt-2">
|
||||
<a href="{{ request.path }}"
|
||||
class="text-blue-600 hover:text-blue-500"
|
||||
hx-get="{{ request.path }}"
|
||||
hx-target="#results-container"
|
||||
hx-push-url="true">
|
||||
Clear all filters
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}"
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
hx-get="?page={{ page_obj.previous_page_number }}"
|
||||
hx-target="#results-container"
|
||||
hx-push-url="true">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}"
|
||||
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
hx-get="?page={{ page_obj.next_page_number }}"
|
||||
hx-target="#results-container"
|
||||
hx-push-url="true">
|
||||
Next
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700">
|
||||
Showing <span class="font-medium">{{ page_obj.start_index }}</span>
|
||||
to <span class="font-medium">{{ page_obj.end_index }}</span>
|
||||
of <span class="font-medium">{{ page_obj.paginator.count }}</span>
|
||||
results
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex rounded-md shadow-xs -space-x-px" aria-label="Pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}"
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||
hx-get="?page={{ page_obj.previous_page_number }}"
|
||||
hx-target="#results-container"
|
||||
hx-push-url="true">
|
||||
<span class="sr-only">Previous</span>
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% for i in page_obj.paginator.page_range %}
|
||||
{% if i == page_obj.number %}
|
||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">
|
||||
{{ i }}
|
||||
</span>
|
||||
{% elif i > page_obj.number|add:"-3" and i < page_obj.number|add:"3" %}
|
||||
<a href="?page={{ i }}"
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||
hx-get="?page={{ i }}"
|
||||
hx-target="#results-container"
|
||||
hx-push-url="true">
|
||||
{{ i }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}"
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||
hx-get="?page={{ page_obj.next_page_number }}"
|
||||
hx-target="#results-container"
|
||||
hx-push-url="true">
|
||||
<span class="sr-only">Next</span>
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
78
templates/core/search/partials/ride_search_results.html
Normal file
78
templates/core/search/partials/ride_search_results.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% load static %}
|
||||
|
||||
<div id="ride-search-results" class="mt-4">
|
||||
{% if rides %}
|
||||
<div class="space-y-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Found {{ rides|length }} ride{{ rides|length|pluralize }}
|
||||
</div>
|
||||
|
||||
{% for ride in rides %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xs border border-gray-200 dark:border-gray-700 p-4 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<a href="{% url 'parks:rides:ride_detail' park_slug=ride.park.slug ride_slug=ride.slug %}"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400">
|
||||
{{ ride.name }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
at <a href="{% url 'parks:park_detail' slug=ride.park.slug %}"
|
||||
class="text-blue-600 dark:text-blue-400 hover:underline">
|
||||
{{ ride.park.name }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if ride.description %}
|
||||
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300 line-clamp-2">
|
||||
{{ ride.description|truncatewords:20 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{% if ride.get_category_display %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
{{ ride.get_category_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.status %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
{% if ride.status == 'OPERATING' %}bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200
|
||||
{% elif ride.status == 'CLOSED_TEMP' %}bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200
|
||||
{% else %}bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200{% endif %}">
|
||||
{{ ride.get_status_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if ride.photos.exists %}
|
||||
<div class="ml-4 shrink-0">
|
||||
{% with ride.photos.first as photo %}
|
||||
<img src="{{ photo.image.url }}"
|
||||
alt="{{ ride.name }}"
|
||||
class="w-16 h-16 rounded-lg object-cover">
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<div class="text-gray-500 dark:text-gray-400">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No rides found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Try adjusting your search criteria.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
300
templates/core/search/results.html
Normal file
300
templates/core/search/results.html
Normal file
@@ -0,0 +1,300 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Search Parks - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Top Filter Bar -->
|
||||
{% include "core/search/filters.html" %}
|
||||
|
||||
<!-- Results Section -->
|
||||
<div id="search-results">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<!-- Consolidated Search and View Controls -->
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<!-- Left side: Search bar with results count -->
|
||||
<div class="flex-1 max-w-md">
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
name="search"
|
||||
value="{{ request.GET.search|default:'' }}"
|
||||
placeholder="Search parks by name, location..."
|
||||
hx-get="{% url 'parks:park_list' %}"
|
||||
hx-target="#results-container"
|
||||
hx-trigger="input changed delay:500ms"
|
||||
hx-indicator="#loading-indicator"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
class="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Parks ({{ results.count|default:0 }} found)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side: View switching buttons -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex bg-gray-100 dark:bg-gray-700 rounded-lg p-1" x-data="{ viewMode: '{{ request.GET.view_mode|default:'grid' }}' }">
|
||||
<button type="button"
|
||||
@click="viewMode = 'grid'; switchView('grid')"
|
||||
:class="viewMode === 'grid' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="viewMode = 'list'; switchView('list')"
|
||||
:class="viewMode === 'list' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors">
|
||||
<svg class="w-4 h-4" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Container -->
|
||||
<div id="results-container" class="divide-y divide-gray-200 dark:divide-gray-700" x-data="{ viewMode: '{{ request.GET.view_mode|default:'grid' }}' }">
|
||||
<!-- Grid View -->
|
||||
<div x-show="viewMode === 'grid'" class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for park in results %}
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg overflow-hidden hover:shadow-md transition-shadow">
|
||||
<!-- Park Image -->
|
||||
<div class="h-48 bg-gray-200 dark:bg-gray-600 overflow-hidden">
|
||||
{% if park.photos.exists %}
|
||||
<img src="{{ park.photos.first.image.url }}"
|
||||
alt="{{ park.name }}"
|
||||
class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<div class="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Park Details -->
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
<a href="{{ park.get_absolute_url }}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||
{{ park.name }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{% if park.formatted_location %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 flex items-center mb-3">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
{{ park.formatted_location }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
{% if park.average_rating %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
{{ park.average_rating }}/10
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ park.get_status_color }} dark:bg-opacity-30">
|
||||
{{ park.get_status_display }}
|
||||
</span>
|
||||
|
||||
{% if park.ride_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 dark:bg-purple-900/30 dark:text-purple-300">
|
||||
{{ park.ride_count }} Ride{{ park.ride_count|pluralize }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if park.description %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{{ park.description|truncatewords:20 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-span-full p-12 text-center text-gray-500 dark:text-gray-400">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">No parks found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Try adjusting your search criteria or filters.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<div x-show="viewMode === 'list'" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for park in results %}
|
||||
<div class="p-6 flex flex-col md:flex-row gap-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||
<!-- Park Image -->
|
||||
<div class="md:w-48 h-32 bg-gray-200 dark:bg-gray-700 rounded-lg overflow-hidden flex-shrink-0">
|
||||
{% if park.photos.exists %}
|
||||
<img src="{{ park.photos.first.image.url }}"
|
||||
alt="{{ park.name }}"
|
||||
class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<div class="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Park Details -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<a href="{{ park.get_absolute_url }}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||
{{ park.name }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{% if park.formatted_location %}
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-400 flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
{{ park.formatted_location }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
{% if park.average_rating %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
{{ park.average_rating }}/10
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ park.get_status_color }} dark:bg-opacity-30">
|
||||
{{ park.get_status_display }}
|
||||
</span>
|
||||
|
||||
{% if park.ride_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 dark:bg-purple-900/30 dark:text-purple-300">
|
||||
{{ park.ride_count }} Ride{{ park.ride_count|pluralize }}
|
||||
</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-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300">
|
||||
{{ park.coaster_count }} Coaster{{ park.coaster_count|pluralize }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if park.description %}
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{{ park.description|truncatewords:30 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if park.opening_date %}
|
||||
<div class="mt-2 text-xs text-gray-500 dark:text-gray-500">
|
||||
Opened: {{ park.opening_date|date:"Y" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="p-12 text-center text-gray-500 dark:text-gray-400">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">No parks found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Try adjusting your search criteria or filters.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Include required scripts #}
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<script src="https://unpkg.com/unpoly@3/unpoly.min.js"></script>
|
||||
|
||||
<script>
|
||||
// View switching functionality
|
||||
function switchView(mode) {
|
||||
// Update URL parameter
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('view_mode', mode);
|
||||
|
||||
// Update the URL without reloading
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
// Store preference in localStorage
|
||||
localStorage.setItem('parkViewMode', mode);
|
||||
}
|
||||
|
||||
// Initialize view mode from URL or localStorage
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlViewMode = urlParams.get('view_mode');
|
||||
const savedViewMode = localStorage.getItem('parkViewMode');
|
||||
const defaultViewMode = urlViewMode || savedViewMode || 'grid';
|
||||
|
||||
// Set initial view mode
|
||||
if (!urlViewMode) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('view_mode', defaultViewMode);
|
||||
window.history.replaceState({}, '', url);
|
||||
}
|
||||
});
|
||||
|
||||
// Enhanced search functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.querySelector('input[name="search"]');
|
||||
if (searchInput) {
|
||||
let searchTimeout;
|
||||
|
||||
// Preserve view mode in search requests
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
// Get current view mode
|
||||
const currentViewMode = new URLSearchParams(window.location.search).get('view_mode') || 'grid';
|
||||
|
||||
// Add view mode to the HTMX request
|
||||
const currentUrl = new URL(e.target.getAttribute('hx-get'), window.location.origin);
|
||||
currentUrl.searchParams.set('view_mode', currentViewMode);
|
||||
currentUrl.searchParams.set('search', e.target.value);
|
||||
|
||||
// Update the hx-get attribute
|
||||
e.target.setAttribute('hx-get', currentUrl.pathname + currentUrl.search);
|
||||
|
||||
// Trigger the HTMX request
|
||||
htmx.trigger(e.target, 'input');
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
87
templates/core/search/ride_search.html
Normal file
87
templates/core/search/ride_search.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Search Rides - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Search Rides</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Find rides across all theme parks</p>
|
||||
</div>
|
||||
|
||||
<!-- Search Form -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xs border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<form method="get"
|
||||
hx-get="{% url 'search:ride_search' %}"
|
||||
hx-target="#ride-search-results"
|
||||
hx-trigger="input changed delay:300ms from:input[name='ride'], submit"
|
||||
hx-indicator="#search-loading">
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="{{ search_form.ride.id_for_label }}"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Search Rides
|
||||
</label>
|
||||
{{ search_form.ride }}
|
||||
{% if search_form.ride.help_text %}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ search_form.ride.help_text }}</p>
|
||||
{% endif %}
|
||||
{% if search_form.ride.errors %}
|
||||
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{{ search_form.ride.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-xs text-white bg-blue-600 hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
Search
|
||||
</button>
|
||||
|
||||
<div id="search-loading" class="htmx-indicator">
|
||||
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Searching...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Search Results -->
|
||||
<div id="ride-search-results">
|
||||
{% include "search/partials/ride_search_results.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Clear search results when input is empty
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.querySelector('input[name="ride"]');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
if (this.value.trim() === '') {
|
||||
const resultsContainer = document.getElementById('ride-search-results');
|
||||
if (resultsContainer) {
|
||||
resultsContainer.innerHTML = '<div class="text-center text-gray-500 dark:text-gray-400 py-8">Start typing to search for rides...</div>';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user