mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 10:51:08 -05:00
- Add HTMX-powered filtering with instant updates - Add smooth transitions and loading states - Improve visual hierarchy and styling - Add review notes functionality - Add confirmation dialogs for actions - Make navigation sticky - Add hover effects and visual feedback - Improve dark mode support
226 lines
10 KiB
HTML
226 lines
10 KiB
HTML
{% extends 'base/base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Parks - ThrillWiki{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container px-4 mx-auto">
|
|
<div class="flex flex-col items-start justify-between gap-4 mb-6 md:flex-row md:items-center">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">All Parks</h1>
|
|
<div hx-get="{% url 'parks:add_park_button' %}" hx-trigger="load"></div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="p-4 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<form id="park-filters" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"
|
|
hx-get="{% url 'parks:park_list' %}"
|
|
hx-trigger="change from:select, input from:input[type='text'] delay:500ms, click from:.status-filter"
|
|
hx-target="#parks-grid"
|
|
hx-push-url="true">
|
|
<div>
|
|
<label for="search" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Search</label>
|
|
<input type="text" name="search" id="search"
|
|
value="{{ current_filters.search }}"
|
|
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
placeholder="Search parks...">
|
|
</div>
|
|
|
|
<!-- Country Field -->
|
|
<div x-data="locationSearch('country')" class="relative">
|
|
<label for="country" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Country</label>
|
|
<input type="text"
|
|
name="country"
|
|
id="country"
|
|
x-model="query"
|
|
@input.debounce.300ms="search()"
|
|
@focus="search()"
|
|
@click.away="results = []"
|
|
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
placeholder="Select country..."
|
|
value="{{ current_filters.country }}"
|
|
autocomplete="off">
|
|
<!-- Results Dropdown -->
|
|
<ul x-show="results.length > 0"
|
|
x-cloak
|
|
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
|
|
<template x-for="result in results" :key="result.address.country">
|
|
<li @click="select(result)"
|
|
x-text="result.address.country"
|
|
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Region Field -->
|
|
<div x-data="locationSearch('state')" class="relative">
|
|
<label for="region" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">State/Region</label>
|
|
<input type="text"
|
|
name="region"
|
|
id="region"
|
|
x-model="query"
|
|
@input.debounce.300ms="search()"
|
|
@focus="search()"
|
|
@click.away="results = []"
|
|
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
placeholder="Select state/region..."
|
|
value="{{ current_filters.region }}"
|
|
autocomplete="off">
|
|
<!-- Results Dropdown -->
|
|
<ul x-show="results.length > 0"
|
|
x-cloak
|
|
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
|
|
<template x-for="result in results" :key="result.address.state">
|
|
<li @click="select(result)"
|
|
x-text="result.address.state"
|
|
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- City Field -->
|
|
<div x-data="locationSearch('city')" class="relative">
|
|
<label for="city" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">City</label>
|
|
<input type="text"
|
|
name="city"
|
|
id="city"
|
|
x-model="query"
|
|
@input.debounce.300ms="search()"
|
|
@focus="search()"
|
|
@click.away="results = []"
|
|
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
placeholder="Select city..."
|
|
value="{{ current_filters.city }}"
|
|
autocomplete="off">
|
|
<!-- Results Dropdown -->
|
|
<ul x-show="results.length > 0"
|
|
x-cloak
|
|
class="absolute z-50 w-full py-1 mt-1 overflow-auto bg-white rounded-md shadow-lg dark:bg-gray-700 max-h-60">
|
|
<template x-for="result in results" :key="result.address.city">
|
|
<li @click="select(result)"
|
|
x-text="result.address.city"
|
|
class="px-4 py-2 text-gray-700 cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Hidden inputs for selected statuses -->
|
|
{% for status in current_filters.statuses %}
|
|
<input type="hidden" name="status" value="{{ status }}">
|
|
{% endfor %}
|
|
</form>
|
|
|
|
<!-- Status Filter Icons -->
|
|
<div class="flex flex-wrap gap-2 mt-4">
|
|
<label class="block w-full mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Status Filter</label>
|
|
<button type="button"
|
|
class="status-filter status-badge status-operating {% if 'OPERATING' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
|
|
data-status="OPERATING"
|
|
onclick="toggleStatus(this, 'OPERATING')">
|
|
<i class="mr-1 fas fa-check-circle"></i>Operating
|
|
</button>
|
|
<button type="button"
|
|
class="status-filter status-badge status-closed {% if 'CLOSED_TEMP' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
|
|
data-status="CLOSED_TEMP"
|
|
onclick="toggleStatus(this, 'CLOSED_TEMP')">
|
|
<i class="mr-1 fas fa-clock"></i>Temporarily Closed
|
|
</button>
|
|
<button type="button"
|
|
class="status-filter status-badge status-closed {% if 'CLOSED_PERM' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
|
|
data-status="CLOSED_PERM"
|
|
onclick="toggleStatus(this, 'CLOSED_PERM')">
|
|
<i class="mr-1 fas fa-times-circle"></i>Permanently Closed
|
|
</button>
|
|
<button type="button"
|
|
class="status-filter status-badge status-construction {% if 'UNDER_CONSTRUCTION' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
|
|
data-status="UNDER_CONSTRUCTION"
|
|
onclick="toggleStatus(this, 'UNDER_CONSTRUCTION')">
|
|
<i class="mr-1 fas fa-hard-hat"></i>Under Construction
|
|
</button>
|
|
<button type="button"
|
|
class="status-filter status-badge status-demolished {% if 'DEMOLISHED' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
|
|
data-status="DEMOLISHED"
|
|
onclick="toggleStatus(this, 'DEMOLISHED')">
|
|
<i class="mr-1 fas fa-ban"></i>Demolished
|
|
</button>
|
|
<button type="button"
|
|
class="status-filter status-badge status-relocated {% if 'RELOCATED' in current_filters.statuses %}ring-2 ring-blue-500{% endif %}"
|
|
data-status="RELOCATED"
|
|
onclick="toggleStatus(this, 'RELOCATED')">
|
|
<i class="mr-1 fas fa-truck-moving"></i>Relocated
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Parks Grid -->
|
|
<div id="parks-grid">
|
|
{% include "parks/partials/park_list.html" %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function locationSearch(type) {
|
|
return {
|
|
query: '',
|
|
results: [],
|
|
async search() {
|
|
if (!this.query.trim()) {
|
|
this.results = [];
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/location/search/?q=${encodeURIComponent(this.query)}&type=${type}&filter_parks=true`);
|
|
const data = await response.json();
|
|
this.results = data.results;
|
|
} catch (error) {
|
|
console.error('Search failed:', error);
|
|
this.results = [];
|
|
}
|
|
},
|
|
select(result) {
|
|
let value = '';
|
|
switch (type) {
|
|
case 'country':
|
|
value = result.address.country;
|
|
break;
|
|
case 'state':
|
|
value = result.address.state;
|
|
break;
|
|
case 'city':
|
|
value = result.address.city;
|
|
break;
|
|
}
|
|
this.query = value;
|
|
this.results = [];
|
|
document.getElementById('park-filters').dispatchEvent(new Event('change'));
|
|
}
|
|
};
|
|
}
|
|
|
|
function toggleStatus(button, status) {
|
|
const form = document.getElementById('park-filters');
|
|
const existingInputs = form.querySelectorAll(`input[name="status"][value="${status}"]`);
|
|
|
|
if (existingInputs.length > 0) {
|
|
// Status is already selected, remove it
|
|
existingInputs.forEach(input => input.remove());
|
|
button.classList.remove('ring-2', 'ring-blue-500');
|
|
} else {
|
|
// Status is not selected, add it
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'status';
|
|
input.value = status;
|
|
form.appendChild(input);
|
|
button.classList.add('ring-2', 'ring-blue-500');
|
|
}
|
|
|
|
// Trigger form submission
|
|
form.dispatchEvent(new Event('change'));
|
|
}
|
|
</script>
|
|
{% endblock %}
|