mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:11:08 -05:00
feat: major project restructure - move Django to backend dir and fix critical imports
- Restructure project: moved Django backend to backend/ directory - Add frontend/ directory for future Next.js application - Add shared/ directory for common resources - Fix critical Django import errors: - Add missing sys.path modification for apps directory - Fix undefined CATEGORY_CHOICES imports in rides module - Fix media migration undefined references - Remove unused imports and f-strings without placeholders - Install missing django-environ dependency - Django server now runs without ModuleNotFoundError - Update .gitignore and README for new structure - Add pnpm workspace configuration for monorepo setup
This commit is contained in:
@@ -1,464 +0,0 @@
|
||||
{# Enhanced filter form with modern UI/UX design - timestamp: 2025-08-21 #}
|
||||
{% load static %}
|
||||
|
||||
<div class="filter-container" x-data="filterManager()" x-init="init()">
|
||||
{# Quick Filter Presets - Enhanced Design #}
|
||||
<div class="mb-8 bg-gradient-to-br from-white via-gray-50 to-white dark:from-gray-800 dark:via-gray-800 dark:to-gray-900 rounded-xl shadow-lg border border-gray-200/50 dark:border-gray-700/50 p-6 backdrop-blur-sm">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-2 h-8 bg-gradient-to-b from-blue-500 to-purple-600 rounded-full"></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Quick Filters</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Find parks instantly with preset filters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">Live filtering</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<button type="button"
|
||||
@click="applyQuickFilter('disney')"
|
||||
class="group relative overflow-hidden rounded-xl p-4 transition-all duration-300 ease-out transform hover:scale-105 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
:class="isQuickFilterActive('disney') ? 'bg-gradient-to-br from-blue-500 to-blue-600 text-white shadow-lg ring-2 ring-blue-500/20' : 'bg-white dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 hover:bg-blue-50 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'"
|
||||
title="Show Disney Parks">
|
||||
<div class="flex flex-col items-center text-center space-y-2">
|
||||
<div class="relative">
|
||||
<svg class="w-8 h-8 transition-transform duration-300 group-hover:rotate-12" 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>
|
||||
<div class="absolute -top-1 -right-1 w-3 h-3 bg-yellow-400 rounded-full animate-ping" x-show="isQuickFilterActive('disney')"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-sm">Disney Parks</div>
|
||||
<div class="text-xs opacity-75" x-show="quickFilterCounts.disney" x-text="`${quickFilterCounts.disney} parks`"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-400/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
@click="applyQuickFilter('coasters')"
|
||||
class="group relative overflow-hidden rounded-xl p-4 transition-all duration-300 ease-out transform hover:scale-105 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
|
||||
:class="isQuickFilterActive('coasters') ? 'bg-gradient-to-br from-green-500 to-green-600 text-white shadow-lg ring-2 ring-green-500/20' : 'bg-white dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 hover:bg-green-50 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'"
|
||||
title="Parks with Roller Coasters">
|
||||
<div class="flex flex-col items-center text-center space-y-2">
|
||||
<div class="relative">
|
||||
<svg class="w-8 h-8 transition-transform duration-300 group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.94-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||
</svg>
|
||||
<div class="absolute -top-1 -right-1 w-3 h-3 bg-red-400 rounded-full animate-ping" x-show="isQuickFilterActive('coasters')"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-sm">With Coasters</div>
|
||||
<div class="text-xs opacity-75" x-show="quickFilterCounts.coasters" x-text="`${quickFilterCounts.coasters} parks`"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-green-400/10 to-emerald-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
@click="applyQuickFilter('top_rated')"
|
||||
class="group relative overflow-hidden rounded-xl p-4 transition-all duration-300 ease-out transform hover:scale-105 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2"
|
||||
:class="isQuickFilterActive('top_rated') ? 'bg-gradient-to-br from-yellow-500 to-yellow-600 text-white shadow-lg ring-2 ring-yellow-500/20' : 'bg-white dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 hover:bg-yellow-50 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'"
|
||||
title="Highly Rated Parks (4+ stars)">
|
||||
<div class="flex flex-col items-center text-center space-y-2">
|
||||
<div class="relative">
|
||||
<svg class="w-8 h-8 transition-transform duration-300 group-hover:rotate-12" 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>
|
||||
<div class="absolute -top-1 -right-1 w-3 h-3 bg-yellow-400 rounded-full animate-ping" x-show="isQuickFilterActive('top_rated')"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-sm">Top Rated</div>
|
||||
<div class="text-xs opacity-75" x-show="quickFilterCounts.top_rated" x-text="`${quickFilterCounts.top_rated} parks`"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-yellow-400/10 to-orange-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
@click="applyQuickFilter('major_parks')"
|
||||
class="group relative overflow-hidden rounded-xl p-4 transition-all duration-300 ease-out transform hover:scale-105 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
|
||||
:class="isQuickFilterActive('major_parks') ? 'bg-gradient-to-br from-purple-500 to-purple-600 text-white shadow-lg ring-2 ring-purple-500/20' : 'bg-white dark:bg-gray-700/50 text-gray-700 dark:text-gray-300 hover:bg-purple-50 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'"
|
||||
title="Major Theme Parks (10+ rides)">
|
||||
<div class="flex flex-col items-center text-center space-y-2">
|
||||
<div class="relative">
|
||||
<svg class="w-8 h-8 transition-transform duration-300 group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
<div class="absolute -top-1 -right-1 w-3 h-3 bg-purple-400 rounded-full animate-ping" x-show="isQuickFilterActive('major_parks')"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-sm">Major Parks</div>
|
||||
<div class="text-xs opacity-75" x-show="quickFilterCounts.major_parks" x-text="`${quickFilterCounts.major_parks} parks`"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-400/10 to-pink-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Mobile Filter Toggle #}
|
||||
<div class="lg:hidden bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-6">
|
||||
<button @click="toggleMobileFilters()" type="button" class="w-full flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors duration-200">
|
||||
<span class="font-medium text-gray-900 dark:text-white">
|
||||
<span class="mr-2">
|
||||
<svg class="w-5 h-5 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
|
||||
</svg>
|
||||
</span>
|
||||
Filter Options
|
||||
<span x-show="activeFilterCount > 0"
|
||||
class="ml-2 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-800 dark:text-blue-100"
|
||||
x-text="activeFilterCount"></span>
|
||||
</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">
|
||||
<svg class="w-5 h-5 transition-transform duration-200" :class="{'rotate-180': mobileFiltersOpen}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Filter Form #}
|
||||
<form hx-get="{{ request.path }}"
|
||||
hx-trigger="change delay:300ms, submit"
|
||||
hx-target="#results-container"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#filter-loading"
|
||||
class="space-y-6"
|
||||
x-show="mobileFiltersOpen || $screen('lg')"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
@change="updateActiveFilterCount()">
|
||||
|
||||
{# Loading Indicator #}
|
||||
<div id="filter-loading" 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>
|
||||
Updating results...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Active Filters Summary #}
|
||||
<div x-show="activeFilterCount > 0"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 shadow-xs border border-blue-100 dark:border-blue-800">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Active Filters</h3>
|
||||
<p class="text-xs text-blue-600 dark:text-blue-300 mt-1" x-text="`${activeFilterCount} filter${activeFilterCount !== 1 ? 's' : ''} applied`"></p>
|
||||
</div>
|
||||
<button type="button"
|
||||
@click="clearAllFilters()"
|
||||
class="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 hover:underline focus:outline-none focus:underline transition-colors duration-150"
|
||||
aria-label="Clear all filters">
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Filter Groups #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div class="p-6" x-data="{ expanded: true }">
|
||||
{# Group Header #}
|
||||
<button type="button"
|
||||
@click="expanded = !expanded"
|
||||
class="w-full flex justify-between items-center text-left focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-md p-1 -m-1"
|
||||
:aria-expanded="expanded"
|
||||
aria-controls="filter-group-1">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Search & Filter Options</h3>
|
||||
<svg class="w-5 h-5 text-gray-500 dark:text-gray-400 transform transition-transform duration-200"
|
||||
:class="{'rotate-180': !expanded}"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true">
|
||||
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{# Group Content #}
|
||||
<div id="filter-group-1"
|
||||
class="mt-4 space-y-4"
|
||||
x-show="expanded"
|
||||
x-collapse>
|
||||
{% for field in filter.form %}
|
||||
<div class="filter-field">
|
||||
<label for="{{ field.id_for_label }}"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 flex items-center">
|
||||
{{ field.label }}
|
||||
{% if field.help_text %}
|
||||
<button type="button"
|
||||
class="ml-2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 focus:outline-none"
|
||||
@click="$tooltip('{{ field.help_text|escapejs }}', $event)"
|
||||
aria-label="Help for {{ field.label }}">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="mt-2 relative">
|
||||
<div class="form-field-wrapper">
|
||||
{% if field.field.widget.input_type == 'text' or field.field.widget.input_type == 'search' %}
|
||||
<input type="{{ field.field.widget.input_type }}"
|
||||
name="{{ field.name }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
value="{{ field.value|default_if_none:'' }}"
|
||||
placeholder="{{ field.field.widget.attrs.placeholder|default:'' }}"
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm transition-colors duration-200">
|
||||
{% elif field.field.widget.input_type == 'number' %}
|
||||
<input type="number"
|
||||
name="{{ field.name }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
value="{{ field.value|default_if_none:'' }}"
|
||||
min="{{ field.field.widget.attrs.min|default:'' }}"
|
||||
max="{{ field.field.widget.attrs.max|default:'' }}"
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm transition-colors duration-200">
|
||||
{% elif field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="{{ field.name }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
{% if field.value %}checked{% endif %}
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded transition-colors duration-200">
|
||||
<label for="{{ field.id_for_label }}" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
</div>
|
||||
{% elif field.field.widget.input_type == 'select' %}
|
||||
<select name="{{ field.name }}"
|
||||
id="{{ field.id_for_label }}"
|
||||
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm transition-colors duration-200">
|
||||
{% for choice in field.field.choices %}
|
||||
<option value="{{ choice.0 }}" {% if choice.0 == field.value %}selected{% endif %}>{{ choice.1 }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if field.field.widget.input_type == 'select' %}
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 dark:text-gray-300">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Mobile Apply Button #}
|
||||
<div class="lg:hidden">
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="isLoading">
|
||||
<span x-show="!isLoading">Apply Filters</span>
|
||||
<span x-show="isLoading" class="flex items-center justify-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>
|
||||
Applying...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Enhanced Scripts #}
|
||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('filterManager', () => ({
|
||||
mobileFiltersOpen: false,
|
||||
activeFilterCount: 0,
|
||||
isLoading: false,
|
||||
quickFilterCounts: {
|
||||
disney: 0,
|
||||
coasters: 0,
|
||||
top_rated: 0,
|
||||
major_parks: 0
|
||||
},
|
||||
|
||||
init() {
|
||||
this.updateActiveFilterCount();
|
||||
this.loadQuickFilterCounts();
|
||||
|
||||
// Listen for HTMX events
|
||||
document.addEventListener('htmx:beforeRequest', () => {
|
||||
this.isLoading = true;
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', () => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:responseError', () => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
toggleMobileFilters() {
|
||||
this.mobileFiltersOpen = !this.mobileFiltersOpen;
|
||||
},
|
||||
|
||||
updateActiveFilterCount() {
|
||||
// Count active filters from form inputs
|
||||
const form = this.$el.querySelector('form');
|
||||
if (!form) return;
|
||||
|
||||
let count = 0;
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
|
||||
inputs.forEach(input => {
|
||||
if (input.name && input.value && input.value !== '' && input.value !== 'all') {
|
||||
// Skip hidden fields and empty values
|
||||
if (input.type !== 'hidden' && input.value !== '0') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.activeFilterCount = count;
|
||||
},
|
||||
|
||||
clearAllFilters() {
|
||||
const form = this.$el.querySelector('form');
|
||||
if (!form) return;
|
||||
|
||||
// Clear all form inputs
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach(input => {
|
||||
if (input.type === 'checkbox' || input.type === 'radio') {
|
||||
input.checked = false;
|
||||
} else if (input.type !== 'hidden') {
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger form submission
|
||||
this.activeFilterCount = 0;
|
||||
htmx.trigger(form, 'submit');
|
||||
},
|
||||
|
||||
applyQuickFilter(filterType) {
|
||||
const form = this.$el.querySelector('form');
|
||||
if (!form) return;
|
||||
|
||||
// Clear existing filters first
|
||||
this.clearAllFilters();
|
||||
|
||||
// Apply specific filter based on type
|
||||
switch(filterType) {
|
||||
case 'disney':
|
||||
const parkTypeField = form.querySelector('[name="park_type"]');
|
||||
if (parkTypeField) parkTypeField.value = 'disney';
|
||||
break;
|
||||
|
||||
case 'coasters':
|
||||
const coastersField = form.querySelector('[name="has_coasters"]');
|
||||
if (coastersField) coastersField.checked = true;
|
||||
break;
|
||||
|
||||
case 'top_rated':
|
||||
const ratingField = form.querySelector('[name="min_rating"]');
|
||||
if (ratingField) ratingField.value = '4';
|
||||
break;
|
||||
|
||||
case 'major_parks':
|
||||
const bigParksField = form.querySelector('[name="big_parks_only"]');
|
||||
if (bigParksField) bigParksField.checked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
this.updateActiveFilterCount();
|
||||
htmx.trigger(form, 'submit');
|
||||
},
|
||||
|
||||
isQuickFilterActive(filterType) {
|
||||
const form = this.$el.querySelector('form');
|
||||
if (!form) return false;
|
||||
|
||||
switch(filterType) {
|
||||
case 'disney':
|
||||
const parkTypeField = form.querySelector('[name="park_type"]');
|
||||
return parkTypeField && parkTypeField.value === 'disney';
|
||||
|
||||
case 'coasters':
|
||||
const coastersField = form.querySelector('[name="has_coasters"]');
|
||||
return coastersField && coastersField.checked;
|
||||
|
||||
case 'top_rated':
|
||||
const ratingField = form.querySelector('[name="min_rating"]');
|
||||
return ratingField && ratingField.value === '4';
|
||||
|
||||
case 'major_parks':
|
||||
const bigParksField = form.querySelector('[name="big_parks_only"]');
|
||||
return bigParksField && bigParksField.checked;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
loadQuickFilterCounts() {
|
||||
// This would typically fetch from an API endpoint
|
||||
// For now, set some placeholder values
|
||||
this.quickFilterCounts = {
|
||||
disney: 12,
|
||||
coasters: 156,
|
||||
top_rated: 89,
|
||||
major_parks: 78
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
// Tooltip directive
|
||||
Alpine.directive('tooltip', (el, { expression }) => {
|
||||
el._tooltip = expression;
|
||||
});
|
||||
|
||||
// Global tooltip function
|
||||
window.$tooltip = function(text, event) {
|
||||
// Simple tooltip implementation - could be enhanced with a library
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'fixed z-50 bg-gray-900 text-white text-sm rounded py-1 px-2 pointer-events-none';
|
||||
tooltip.textContent = text;
|
||||
tooltip.style.left = event.pageX + 10 + 'px';
|
||||
tooltip.style.top = event.pageY - 30 + 'px';
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.parentNode.removeChild(tooltip);
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
});
|
||||
|
||||
// Enhanced debouncing for text inputs
|
||||
document.addEventListener('input', function(e) {
|
||||
if (e.target.type === 'text' || e.target.type === 'search') {
|
||||
clearTimeout(e.target._debounceTimer);
|
||||
e.target._debounceTimer = setTimeout(() => {
|
||||
htmx.trigger(e.target.form, 'change');
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<form hx-get="{% url 'search:search' %}" hx-target="#search-results" hx-swap="outerHTML" class="space-y-4">
|
||||
{% for field in filters.form %}
|
||||
<div class="flex flex-col">
|
||||
<label for="{{ field.id_for_label }}" class="text-sm font-medium text-gray-700">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
{{ field }}
|
||||
</div>
|
||||
{% if field.help_text %}
|
||||
<p class="text-sm text-gray-500">{{ field.help_text }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="flex justify-between">
|
||||
<button type="submit"
|
||||
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Apply Filters
|
||||
</button>
|
||||
{% if applied_filters %}
|
||||
<a href="{% url 'search:search' %}"
|
||||
class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-xs text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Clear Filters
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,64 +0,0 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Parks - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
{# Filters Sidebar #}
|
||||
<div class="lg:w-1/4 lg:sticky lg:top-4 lg:self-start">
|
||||
{% block filter_section %}
|
||||
<!-- DEBUG: base filtered_list.html filter_section block - timestamp: 2025-08-21 -->
|
||||
{% include "core/search/components/filter_form.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{# Results Section #}
|
||||
<div class="lg:w-3/4">
|
||||
<div id="results-container">
|
||||
{# Result count and sorting #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6">
|
||||
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Parks
|
||||
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">({{ page_obj.paginator.count }} found)</span>
|
||||
</h2>
|
||||
|
||||
{% block list_actions %}
|
||||
{# Custom actions can be added here by extending views #}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Results list #}
|
||||
{% block results_list %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
{% include results_template|default:"search/partials/generic_results.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# Pagination #}
|
||||
{% if is_paginated %}
|
||||
<div class="mt-6">
|
||||
{% include "search/components/pagination.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
@@ -1,332 +0,0 @@
|
||||
{% extends "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 %}
|
||||
@@ -1,134 +0,0 @@
|
||||
<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 %}
|
||||
@@ -1,78 +0,0 @@
|
||||
{% 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>
|
||||
@@ -1,100 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Search Parks - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex flex-col lg:flex-row gap-8">
|
||||
<!-- Filters Sidebar -->
|
||||
<div class="lg:w-1/4">
|
||||
<div class="bg-white p-6 rounded-lg shadow">
|
||||
<h2 class="text-xl font-bold mb-4">Filter Parks</h2>
|
||||
{% include "search/filters.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section -->
|
||||
<div class="lg:w-3/4" id="search-results">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="p-6 border-b">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold">
|
||||
Search Results
|
||||
<span class="text-sm font-normal text-gray-500">({{ results.count }} found)</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divide-y">
|
||||
{% for park in results %}
|
||||
<div class="p-6 flex flex-col md:flex-row gap-4">
|
||||
<!-- Park Image -->
|
||||
<div class="md:w-48 h-32 bg-gray-200 rounded-lg 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">
|
||||
No Image
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Park Details -->
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold">
|
||||
<a href="{{ park.get_absolute_url }}" class="hover:text-blue-600">
|
||||
{{ park.name }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
{% if park.formatted_location %}
|
||||
<p>{{ park.formatted_location }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 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">
|
||||
{{ park.average_rating }} ★
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<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.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">
|
||||
{{ 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-yellow-100 text-yellow-800">
|
||||
{{ park.coaster_count }} Coasters
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if park.description %}
|
||||
<p class="mt-2 text-sm text-gray-600 line-clamp-2">
|
||||
{{ park.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="p-6 text-center text-gray-500">
|
||||
No parks found matching your criteria.
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,87 +0,0 @@
|
||||
{% 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