mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:11:08 -05:00
Add park and ride card components with advanced search functionality
- Implemented park card component with image, status badge, favorite button, and quick stats overlay. - Developed ride card component featuring thrill level badge, status badge, favorite button, and detailed stats. - Created advanced search page with filters for parks and rides, including location, type, status, and thrill level. - Added dynamic quick search functionality with results display. - Enhanced user experience with JavaScript for filter toggling, range slider updates, and view switching. - Included custom CSS for improved styling of checkboxes and search results layout.
This commit is contained in:
@@ -48,7 +48,6 @@
|
||||
<!-- Preload Critical Resources -->
|
||||
{% block critical_resources %}
|
||||
<link rel="preload" href="{% static 'css/tailwind.css' %}" as="style" />
|
||||
<link rel="preload" href="{% static 'js/theme.js' %}?v={{ version|default:'1.0' }}" as="script" />
|
||||
<link rel="preload" href="{% static 'js/alpine.min.js' %}?v={{ version|default:'1.0' }}" as="script" />
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" as="style" />
|
||||
{% endblock %}
|
||||
@@ -62,23 +61,15 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Prevent flash of wrong theme -->
|
||||
<script src="{% static 'js/theme.js' %}?v={{ version|default:'1.0' }}"></script>
|
||||
|
||||
<!-- HTMX -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Alpine.js Components (must load before Alpine.js) -->
|
||||
<script src="{% static 'js/alpine-components.js' %}?v={{ version|default:'1.0' }}"></script>
|
||||
|
||||
<!-- Alpine.js (must load after components) -->
|
||||
<script defer src="{% static 'js/alpine.min.js' %}?v={{ version|default:'1.0' }}"></script>
|
||||
|
||||
<!-- Location Autocomplete -->
|
||||
<script defer src="{% static 'js/location-autocomplete.js' %}?v={{ version|default:'1.0' }}"></script>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'css/design-system.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'css/components.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'css/alerts.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'css/inline-styles.css' %}" rel="stylesheet" />
|
||||
@@ -181,9 +172,347 @@
|
||||
<!-- Global Toast Container -->
|
||||
<c-toast_container />
|
||||
|
||||
<!-- Custom JavaScript with cache control -->
|
||||
<script src="{% static 'js/main.js' %}?v={{ version|default:'1.0' }}"></script>
|
||||
<script src="{% static 'js/alerts.js' %}?v={{ version|default:'1.0' }}"></script>
|
||||
<!-- AlpineJS Components and Stores (Inline) -->
|
||||
<script>
|
||||
// Global Alpine.js stores and components
|
||||
document.addEventListener('alpine:init', () => {
|
||||
// Global Store for App State
|
||||
Alpine.store('app', {
|
||||
user: null,
|
||||
theme: localStorage.getItem('theme') || 'system',
|
||||
searchQuery: '',
|
||||
notifications: [],
|
||||
|
||||
setUser(user) {
|
||||
this.user = user;
|
||||
},
|
||||
|
||||
setTheme(theme) {
|
||||
this.theme = theme;
|
||||
localStorage.setItem('theme', theme);
|
||||
},
|
||||
|
||||
addNotification(notification) {
|
||||
this.notifications.push({
|
||||
id: Date.now(),
|
||||
...notification
|
||||
});
|
||||
},
|
||||
|
||||
removeNotification(id) {
|
||||
this.notifications = this.notifications.filter(n => n.id !== id);
|
||||
}
|
||||
});
|
||||
|
||||
// Global Toast Store
|
||||
Alpine.store('toast', {
|
||||
toasts: [],
|
||||
|
||||
show(message, type = 'info', duration = 5000) {
|
||||
const id = Date.now() + Math.random();
|
||||
const toast = {
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
visible: true,
|
||||
progress: 100
|
||||
};
|
||||
|
||||
this.toasts.push(toast);
|
||||
|
||||
if (duration > 0) {
|
||||
const interval = setInterval(() => {
|
||||
toast.progress -= (100 / (duration / 100));
|
||||
if (toast.progress <= 0) {
|
||||
clearInterval(interval);
|
||||
this.hide(id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
hide(id) {
|
||||
const toast = this.toasts.find(t => t.id === id);
|
||||
if (toast) {
|
||||
toast.visible = false;
|
||||
setTimeout(() => {
|
||||
this.toasts = this.toasts.filter(t => t.id !== id);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
|
||||
success(message, duration = 5000) {
|
||||
return this.show(message, 'success', duration);
|
||||
},
|
||||
|
||||
error(message, duration = 7000) {
|
||||
return this.show(message, 'error', duration);
|
||||
},
|
||||
|
||||
warning(message, duration = 6000) {
|
||||
return this.show(message, 'warning', duration);
|
||||
},
|
||||
|
||||
info(message, duration = 5000) {
|
||||
return this.show(message, 'info', duration);
|
||||
}
|
||||
});
|
||||
|
||||
// Theme Toggle Component
|
||||
Alpine.data('themeToggle', () => ({
|
||||
theme: localStorage.getItem('theme') || 'system',
|
||||
|
||||
init() {
|
||||
this.updateTheme();
|
||||
|
||||
// Watch for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (this.theme === 'system') {
|
||||
this.updateTheme();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
const themes = ['light', 'dark', 'system'];
|
||||
const currentIndex = themes.indexOf(this.theme);
|
||||
this.theme = themes[(currentIndex + 1) % themes.length];
|
||||
localStorage.setItem('theme', this.theme);
|
||||
this.updateTheme();
|
||||
},
|
||||
|
||||
updateTheme() {
|
||||
const root = document.documentElement;
|
||||
|
||||
if (this.theme === 'dark' ||
|
||||
(this.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Modal Component
|
||||
Alpine.data('modal', (initialOpen = false) => ({
|
||||
open: initialOpen,
|
||||
|
||||
show() {
|
||||
this.open = true;
|
||||
document.body.style.overflow = 'hidden';
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.open = false;
|
||||
document.body.style.overflow = '';
|
||||
},
|
||||
|
||||
toggle() {
|
||||
if (this.open) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Dropdown Component
|
||||
Alpine.data('dropdown', (initialOpen = false) => ({
|
||||
open: initialOpen,
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
show() {
|
||||
this.open = true;
|
||||
}
|
||||
}));
|
||||
|
||||
// Search Component - HTMX-based (NO FETCH API)
|
||||
Alpine.data('searchComponent', () => ({
|
||||
query: '',
|
||||
loading: false,
|
||||
showResults: false,
|
||||
|
||||
init() {
|
||||
// Listen for HTMX events
|
||||
this.$el.addEventListener('htmx:beforeRequest', () => {
|
||||
this.loading = true;
|
||||
});
|
||||
|
||||
this.$el.addEventListener('htmx:afterRequest', () => {
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
this.$el.addEventListener('htmx:afterSettle', () => {
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
this.showResults = resultsContainer && resultsContainer.children.length > 0;
|
||||
});
|
||||
},
|
||||
|
||||
handleInput() {
|
||||
if (this.query.length < 2) {
|
||||
this.showResults = false;
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
if (resultsContainer) {
|
||||
resultsContainer.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
// HTMX will handle the actual search via hx-trigger
|
||||
},
|
||||
|
||||
selectResult(url) {
|
||||
window.location.href = url;
|
||||
this.showResults = false;
|
||||
this.query = '';
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.query = '';
|
||||
this.showResults = false;
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
if (resultsContainer) {
|
||||
resultsContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Browse Menu Component
|
||||
Alpine.data('browseMenu', () => ({
|
||||
open: false,
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
}
|
||||
}));
|
||||
|
||||
// Mobile Menu Component
|
||||
Alpine.data('mobileMenu', () => ({
|
||||
open: false,
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
|
||||
if (this.open) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}));
|
||||
|
||||
// User Menu Component
|
||||
Alpine.data('userMenu', () => ({
|
||||
open: false,
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
}
|
||||
}));
|
||||
|
||||
// Auth Modal Component
|
||||
Alpine.data('authModal', (defaultMode = 'login') => ({
|
||||
open: false,
|
||||
mode: defaultMode,
|
||||
showPassword: false,
|
||||
socialProviders: [
|
||||
{id: 'google', name: 'Google', auth_url: '/accounts/google/login/'},
|
||||
{id: 'discord', name: 'Discord', auth_url: '/accounts/discord/login/'}
|
||||
],
|
||||
|
||||
loginForm: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loginLoading: false,
|
||||
loginError: '',
|
||||
|
||||
registerForm: {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
username: '',
|
||||
password1: '',
|
||||
password2: ''
|
||||
},
|
||||
registerLoading: false,
|
||||
registerError: '',
|
||||
|
||||
init() {
|
||||
this.$watch('open', (value) => {
|
||||
if (value) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
this.resetForms();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
show(mode = 'login') {
|
||||
this.mode = mode;
|
||||
this.open = true;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
switchToLogin() {
|
||||
this.mode = 'login';
|
||||
this.resetForms();
|
||||
},
|
||||
|
||||
switchToRegister() {
|
||||
this.mode = 'register';
|
||||
this.resetForms();
|
||||
},
|
||||
|
||||
resetForms() {
|
||||
this.loginForm = { username: '', password: '' };
|
||||
this.registerForm = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
username: '',
|
||||
password1: '',
|
||||
password2: ''
|
||||
};
|
||||
this.loginError = '';
|
||||
this.registerError = '';
|
||||
this.showPassword = false;
|
||||
},
|
||||
|
||||
getCSRFToken() {
|
||||
const token = document.querySelector('[name=csrfmiddlewaretoken]')?.value ||
|
||||
document.querySelector('meta[name=csrf-token]')?.getAttribute('content') ||
|
||||
document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1];
|
||||
return token || '';
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
166
templates/components/cards/park_card.html
Normal file
166
templates/components/cards/park_card.html
Normal file
@@ -0,0 +1,166 @@
|
||||
<!-- Park Card Component -->
|
||||
<div class="card-park hover-lift group"
|
||||
hx-get="{% url 'parks:detail' park.slug %}"
|
||||
hx-target="#main-content"
|
||||
hx-swap="innerHTML transition:true">
|
||||
|
||||
<!-- Park Image with Overlay -->
|
||||
<div class="relative overflow-hidden rounded-t-2xl">
|
||||
{% if park.featured_image %}
|
||||
<img src="{{ park.featured_image.url }}"
|
||||
alt="{{ park.name }}"
|
||||
class="card-park-image"
|
||||
loading="lazy">
|
||||
{% else %}
|
||||
<div class="card-park-image bg-gradient-to-br from-thrill-primary/20 to-thrill-secondary/20 flex items-center justify-center">
|
||||
<i class="fas fa-map-marked-alt text-6xl text-thrill-primary/40"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Status Badge -->
|
||||
<div class="absolute top-4 left-4">
|
||||
{% if park.status == 'OPERATING' %}
|
||||
<span class="badge-operating">
|
||||
<i class="fas fa-check-circle mr-1"></i>
|
||||
Operating
|
||||
</span>
|
||||
{% elif park.status == 'CONSTRUCTION' %}
|
||||
<span class="badge-construction">
|
||||
<i class="fas fa-hard-hat mr-1"></i>
|
||||
Under Construction
|
||||
</span>
|
||||
{% elif park.status == 'CLOSED_PERMANENTLY' %}
|
||||
<span class="badge-closed">
|
||||
<i class="fas fa-times-circle mr-1"></i>
|
||||
Closed
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Favorite Button -->
|
||||
<div class="absolute top-4 right-4">
|
||||
<button class="w-10 h-10 bg-white/90 dark:bg-neutral-800/90 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white dark:hover:bg-neutral-800 transition-all duration-200 hover:scale-110"
|
||||
hx-post="{% url 'parks:toggle_favorite' park.slug %}"
|
||||
hx-swap="outerHTML"
|
||||
onclick="event.stopPropagation()">
|
||||
{% if park.is_favorited %}
|
||||
<i class="fas fa-heart text-red-500"></i>
|
||||
{% else %}
|
||||
<i class="far fa-heart text-neutral-600 dark:text-neutral-400"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Gradient Overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- Quick Stats Overlay -->
|
||||
<div class="absolute bottom-4 left-4 right-4 transform translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
|
||||
<div class="flex items-center justify-between text-white text-sm">
|
||||
<div class="flex items-center space-x-4">
|
||||
{% if park.ride_count %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-rocket mr-1"></i>
|
||||
{{ park.ride_count }} rides
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if park.area %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-expand-arrows-alt mr-1"></i>
|
||||
{{ park.area }} acres
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if park.rating %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-star text-yellow-400 mr-1"></i>
|
||||
{{ park.rating|floatformat:1 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Park Content -->
|
||||
<div class="card-park-content">
|
||||
<!-- Park Name and Location -->
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-xl font-bold text-neutral-900 dark:text-neutral-100 group-hover:text-thrill-primary transition-colors duration-200">
|
||||
{{ park.name }}
|
||||
</h3>
|
||||
|
||||
{% if park.location %}
|
||||
<div class="flex items-center text-neutral-600 dark:text-neutral-400 text-sm">
|
||||
<i class="fas fa-map-marker-alt mr-2 text-thrill-primary"></i>
|
||||
{{ park.location.city }}{% if park.location.region %}, {{ park.location.region }}{% endif %}
|
||||
{% if park.location.country %}, {{ park.location.country }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Park Description -->
|
||||
{% if park.description %}
|
||||
<p class="text-neutral-600 dark:text-neutral-400 text-sm line-clamp-2">
|
||||
{{ park.description|truncatewords:20 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Park Features/Tags -->
|
||||
{% if park.features.all %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for feature in park.features.all|slice:":3" %}
|
||||
<span class="px-2 py-1 bg-thrill-primary/10 text-thrill-primary text-xs rounded-full">
|
||||
{{ feature.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% if park.features.count > 3 %}
|
||||
<span class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400 text-xs rounded-full">
|
||||
+{{ park.features.count|add:"-3" }} more
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Park Stats -->
|
||||
<div class="flex items-center justify-between pt-4 border-t border-neutral-200/50 dark:border-neutral-700/50">
|
||||
<div class="flex items-center space-x-4 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{% if park.opened_date %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar mr-1"></i>
|
||||
{{ park.opened_date.year }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if park.park_type %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-tag mr-1"></i>
|
||||
{{ park.get_park_type_display }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<button class="btn-primary btn-sm opacity-0 group-hover:opacity-100 transition-all duration-200 transform translate-x-2 group-hover:translate-x-0">
|
||||
<i class="fas fa-arrow-right mr-2"></i>
|
||||
Explore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State Overlay -->
|
||||
<div class="absolute inset-0 bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm rounded-2xl flex items-center justify-center opacity-0 htmx-request:opacity-100 transition-opacity duration-200 pointer-events-none">
|
||||
<div class="loading-spinner opacity-100">
|
||||
<i class="fas fa-spinner text-2xl text-thrill-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSS for line-clamp utility -->
|
||||
<style>
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
271
templates/components/cards/ride_card.html
Normal file
271
templates/components/cards/ride_card.html
Normal file
@@ -0,0 +1,271 @@
|
||||
<!-- Ride Card Component -->
|
||||
<div class="card-ride hover-lift group"
|
||||
hx-get="{% url 'rides:detail' ride.slug %}"
|
||||
hx-target="#main-content"
|
||||
hx-swap="innerHTML transition:true">
|
||||
|
||||
<!-- Ride Image with Overlay -->
|
||||
<div class="relative overflow-hidden rounded-t-2xl">
|
||||
{% if ride.featured_image %}
|
||||
<img src="{{ ride.featured_image.url }}"
|
||||
alt="{{ ride.name }}"
|
||||
class="card-ride-image"
|
||||
loading="lazy">
|
||||
{% else %}
|
||||
<div class="card-ride-image bg-gradient-to-br from-thrill-secondary/20 to-red-500/20 flex items-center justify-center">
|
||||
<i class="fas fa-rocket text-6xl text-thrill-secondary/40"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Thrill Level Badge -->
|
||||
<div class="absolute top-4 left-4">
|
||||
{% if ride.thrill_level == 'EXTREME' %}
|
||||
<span class="badge bg-red-500/90 text-white border-red-500/20 backdrop-blur-sm">
|
||||
<i class="fas fa-fire mr-1"></i>
|
||||
Extreme
|
||||
</span>
|
||||
{% elif ride.thrill_level == 'HIGH' %}
|
||||
<span class="badge bg-orange-500/90 text-white border-orange-500/20 backdrop-blur-sm">
|
||||
<i class="fas fa-bolt mr-1"></i>
|
||||
High Thrill
|
||||
</span>
|
||||
{% elif ride.thrill_level == 'MODERATE' %}
|
||||
<span class="badge bg-yellow-500/90 text-white border-yellow-500/20 backdrop-blur-sm">
|
||||
<i class="fas fa-star mr-1"></i>
|
||||
Moderate
|
||||
</span>
|
||||
{% elif ride.thrill_level == 'MILD' %}
|
||||
<span class="badge bg-green-500/90 text-white border-green-500/20 backdrop-blur-sm">
|
||||
<i class="fas fa-leaf mr-1"></i>
|
||||
Family
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
{% if ride.status != 'OPERATING' %}
|
||||
<div class="absolute top-4 right-4">
|
||||
{% if ride.status == 'CONSTRUCTION' %}
|
||||
<span class="badge-construction backdrop-blur-sm">
|
||||
<i class="fas fa-hard-hat mr-1"></i>
|
||||
Coming Soon
|
||||
</span>
|
||||
{% elif ride.status == 'CLOSED_PERMANENTLY' %}
|
||||
<span class="badge-closed backdrop-blur-sm">
|
||||
<i class="fas fa-times-circle mr-1"></i>
|
||||
Closed
|
||||
</span>
|
||||
{% elif ride.status == 'CLOSED_TEMPORARILY' %}
|
||||
<span class="badge bg-yellow-500/90 text-white border-yellow-500/20 backdrop-blur-sm">
|
||||
<i class="fas fa-pause-circle mr-1"></i>
|
||||
Maintenance
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Favorite Button -->
|
||||
<div class="absolute {% if ride.status != 'OPERATING' %}top-16{% else %}top-4{% endif %} right-4">
|
||||
<button class="w-10 h-10 bg-white/90 dark:bg-neutral-800/90 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white dark:hover:bg-neutral-800 transition-all duration-200 hover:scale-110"
|
||||
hx-post="{% url 'rides:toggle_favorite' ride.slug %}"
|
||||
hx-swap="outerHTML"
|
||||
onclick="event.stopPropagation()">
|
||||
{% if ride.is_favorited %}
|
||||
<i class="fas fa-heart text-red-500"></i>
|
||||
{% else %}
|
||||
<i class="far fa-heart text-neutral-600 dark:text-neutral-400"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Gradient Overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- Quick Stats Overlay -->
|
||||
<div class="absolute bottom-4 left-4 right-4 transform translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
|
||||
<div class="grid grid-cols-2 gap-4 text-white text-sm">
|
||||
{% if ride.height %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-arrows-alt-v mr-2 text-thrill-secondary"></i>
|
||||
<div>
|
||||
<div class="font-semibold">{{ ride.height }}ft</div>
|
||||
<div class="text-xs opacity-75">Height</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.speed %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-tachometer-alt mr-2 text-thrill-secondary"></i>
|
||||
<div>
|
||||
<div class="font-semibold">{{ ride.speed }}mph</div>
|
||||
<div class="text-xs opacity-75">Speed</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.duration %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-clock mr-2 text-thrill-secondary"></i>
|
||||
<div>
|
||||
<div class="font-semibold">{{ ride.duration }}s</div>
|
||||
<div class="text-xs opacity-75">Duration</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.inversions %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-sync-alt mr-2 text-thrill-secondary"></i>
|
||||
<div>
|
||||
<div class="font-semibold">{{ ride.inversions }}</div>
|
||||
<div class="text-xs opacity-75">Inversions</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trending Badge -->
|
||||
{% if ride.is_trending %}
|
||||
<div class="absolute bottom-4 left-4 opacity-100 group-hover:opacity-0 transition-opacity duration-300">
|
||||
<span class="badge bg-gradient-to-r from-pink-500 to-red-500 text-white border-0 pulse-glow">
|
||||
<i class="fas fa-fire mr-1"></i>
|
||||
Trending
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Ride Content -->
|
||||
<div class="card-ride-content">
|
||||
<!-- Ride Name and Park -->
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-lg font-bold text-neutral-900 dark:text-neutral-100 group-hover:text-thrill-secondary transition-colors duration-200">
|
||||
{{ ride.name }}
|
||||
</h3>
|
||||
|
||||
{% if ride.park %}
|
||||
<div class="flex items-center text-neutral-600 dark:text-neutral-400 text-sm">
|
||||
<i class="fas fa-map-marked-alt mr-2 text-thrill-secondary"></i>
|
||||
{{ ride.park.name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Ride Type and Category -->
|
||||
<div class="flex items-center space-x-3">
|
||||
{% if ride.category %}
|
||||
<span class="px-2 py-1 bg-thrill-secondary/10 text-thrill-secondary text-xs rounded-full font-medium">
|
||||
{{ ride.get_category_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.coaster_type %}
|
||||
<span class="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400 text-xs rounded-full">
|
||||
{{ ride.get_coaster_type_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Ride Description -->
|
||||
{% if ride.description %}
|
||||
<p class="text-neutral-600 dark:text-neutral-400 text-sm line-clamp-2">
|
||||
{{ ride.description|truncatewords:15 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Ride Stats -->
|
||||
<div class="flex items-center justify-between pt-3 border-t border-neutral-200/50 dark:border-neutral-700/50">
|
||||
<div class="flex items-center space-x-4 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{% if ride.opened_date %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar mr-1"></i>
|
||||
{{ ride.opened_date.year }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ride.manufacturer %}
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-industry mr-1"></i>
|
||||
{{ ride.manufacturer.name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
{% if ride.rating %}
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center mr-2">
|
||||
{% for i in "12345" %}
|
||||
{% if forloop.counter <= ride.rating %}
|
||||
<i class="fas fa-star text-yellow-400 text-xs"></i>
|
||||
{% else %}
|
||||
<i class="far fa-star text-neutral-300 dark:text-neutral-600 text-xs"></i>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
|
||||
{{ ride.rating|floatformat:1 }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<div class="pt-3">
|
||||
<button class="btn-secondary btn-sm w-full opacity-0 group-hover:opacity-100 transition-all duration-200 transform translate-y-2 group-hover:translate-y-0">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
View Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State Overlay -->
|
||||
<div class="absolute inset-0 bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm rounded-2xl flex items-center justify-center opacity-0 htmx-request:opacity-100 transition-opacity duration-200 pointer-events-none">
|
||||
<div class="loading-spinner opacity-100">
|
||||
<i class="fas fa-spinner text-2xl text-thrill-secondary"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional CSS for enhanced styling -->
|
||||
<style>
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Enhanced pulse animation for trending badge */
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 0 rgba(236, 72, 153, 0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 8px rgba(236, 72, 153, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-glow {
|
||||
animation: pulse-glow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Smooth hover transitions for stats */
|
||||
.card-ride:hover .grid > div {
|
||||
animation: slideInUp 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.card-ride:hover .grid > div:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.card-ride:hover .grid > div:nth-child(3) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.card-ride:hover .grid > div:nth-child(4) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
</style>
|
||||
@@ -1,472 +1,435 @@
|
||||
{% comment %}
|
||||
Enhanced Header Component - Matches React Frontend Design
|
||||
Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
|
||||
{% endcomment %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
<header class="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div class="flex items-center h-14 px-4 gap-3 md:gap-4">
|
||||
|
||||
<!-- Logo and Browse Menu -->
|
||||
<div class="flex items-center gap-4 shrink-0">
|
||||
<!-- Logo -->
|
||||
<a href="{% url 'home' %}" class="flex items-center space-x-2 flex-shrink-0">
|
||||
<div class="w-6 h-6 bg-purple-600 rounded flex items-center justify-center">
|
||||
<span class="text-white text-xs font-bold">TW</span>
|
||||
</div>
|
||||
<span class="font-bold text-lg whitespace-nowrap">ThrillWiki</span>
|
||||
</a>
|
||||
|
||||
<!-- Browse Menu (Desktop) -->
|
||||
<div class="hidden md:block">
|
||||
<div
|
||||
x-data="{ open: false }"
|
||||
@mouseenter="open = true"
|
||||
@mouseleave="open = false"
|
||||
class="relative"
|
||||
>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md hover:bg-accent transition-colors"
|
||||
@click="open = !open"
|
||||
>
|
||||
<i class="fas fa-compass w-4 h-4"></i>
|
||||
Browse
|
||||
<i class="fas fa-chevron-down w-3 h-3 transition-transform duration-200" :class="{ 'rotate-180': open }"></i>
|
||||
</button>
|
||||
|
||||
<!-- Browse Dropdown -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
x-cloak
|
||||
class="absolute left-0 mt-1 w-auto max-w-4xl p-6 bg-background border rounded-lg shadow-lg z-50"
|
||||
>
|
||||
<div class="flex gap-8">
|
||||
<!-- Left Column -->
|
||||
<div class="flex-1 space-y-4 min-w-0">
|
||||
<a
|
||||
href="{% url 'parks:park_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-md hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-map-marker-alt w-4 h-4 mt-0.5 text-muted-foreground group-hover:text-foreground flex-shrink-0"></i>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="font-medium text-sm mb-1 leading-tight">Parks</h3>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">Explore theme parks worldwide</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{% url 'rides:manufacturer_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-md hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-wrench w-4 h-4 mt-0.5 text-muted-foreground group-hover:text-foreground flex-shrink-0"></i>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="font-medium text-sm mb-1 leading-tight">Manufacturers</h3>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">Ride and attraction manufacturers</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{% url 'parks:operator_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-md hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-users w-4 h-4 mt-0.5 text-muted-foreground group-hover:text-foreground flex-shrink-0"></i>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="font-medium text-sm mb-1 leading-tight">Operators</h3>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">Theme park operating companies</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="flex-1 space-y-4 min-w-0">
|
||||
<a
|
||||
href="{% url 'rides:global_ride_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-md hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-rocket w-4 h-4 mt-0.5 text-muted-foreground group-hover:text-foreground flex-shrink-0"></i>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="font-medium text-sm mb-1 leading-tight">Rides</h3>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">Discover rides and attractions</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{% url 'rides:designer_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-md hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-drafting-compass w-4 h-4 mt-0.5 text-muted-foreground group-hover:text-foreground flex-shrink-0"></i>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="font-medium text-sm mb-1 leading-tight">Designers</h3>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">Ride designers and architects</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-start gap-3 p-3 rounded-md hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-trophy w-4 h-4 mt-0.5 text-muted-foreground group-hover:text-foreground flex-shrink-0"></i>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="font-medium text-sm mb-1 leading-tight">Top Lists</h3>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">Community rankings and favorites</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Search Bar -->
|
||||
<div class="hidden md:flex flex-1 min-w-0 justify-center">
|
||||
<!-- Enhanced Search -->
|
||||
<div class="relative w-full min-w-0 max-w-xl lg:max-w-2xl" x-data="searchComponent">
|
||||
<div class="relative">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"></i>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search parks, rides..."
|
||||
class="w-full min-w-0 pl-10 pr-3 h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
x-model="query"
|
||||
@input.debounce.300ms="search()"
|
||||
hx-get="{% url 'search:search' %}"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
hx-target="#search-results"
|
||||
hx-include="this"
|
||||
name="q"
|
||||
/>
|
||||
</div>
|
||||
<!-- Enhanced Navigation Header -->
|
||||
<header class="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
|
||||
x-data="{
|
||||
isOpen: false,
|
||||
isScrolled: false,
|
||||
searchOpen: false,
|
||||
userMenuOpen: false
|
||||
}"
|
||||
x-init="
|
||||
window.addEventListener('scroll', () => {
|
||||
isScrolled = window.scrollY > 20;
|
||||
});
|
||||
"
|
||||
:class="isScrolled ? 'bg-white/95 dark:bg-neutral-900/95 backdrop-blur-xl shadow-xl border-b border-neutral-200/50 dark:border-neutral-700/50' : 'bg-transparent'">
|
||||
|
||||
<nav class="container mx-auto px-6 py-4" role="navigation" aria-label="Main navigation">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
<!-- Logo/Brand -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{% url 'home' %}"
|
||||
class="nav-brand text-2xl md:text-3xl font-bold transition-all duration-300 hover:scale-105"
|
||||
aria-label="ThrillWiki Home">
|
||||
<span class="bg-gradient-to-r from-thrill-primary via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
ThrillWiki
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Search Results Dropdown -->
|
||||
<div
|
||||
id="search-results"
|
||||
x-show="results.length > 0"
|
||||
x-transition
|
||||
x-cloak
|
||||
class="absolute top-full left-0 right-0 mt-1 bg-background border rounded-md shadow-lg z-50 max-h-96 overflow-y-auto"
|
||||
>
|
||||
<!-- Search results will be populated by HTMX -->
|
||||
<!-- Beta Badge -->
|
||||
<div class="hidden sm:block">
|
||||
<span class="badge badge-info text-xs pulse-glow">
|
||||
<i class="fas fa-rocket mr-1"></i>
|
||||
Beta
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Right Side -->
|
||||
<div class="hidden md:flex items-center gap-4 shrink-0">
|
||||
<!-- Search Button -->
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<button
|
||||
x-data="themeToggle"
|
||||
@click="toggleTheme()"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-12 w-12"
|
||||
>
|
||||
<i class="fas fa-sun h-5 w-5 md:h-7 md:w-7 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0 text-lg"></i>
|
||||
<i class="fas fa-moon absolute h-5 w-5 md:h-7 md:w-7 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 text-lg"></i>
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</button>
|
||||
|
||||
<!-- User Icon -->
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||
<button
|
||||
@click="open = !open"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-12 w-12"
|
||||
>
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.profile.avatar %}
|
||||
<img
|
||||
src="{{ user.profile.avatar.url }}"
|
||||
alt="{{ user.get_full_name|default:user.username }}"
|
||||
class="h-8 w-8 rounded-full object-cover"
|
||||
/>
|
||||
{% else %}
|
||||
<div class="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
|
||||
{{ user.get_full_name.0|default:user.username.0|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<i class="fas fa-user h-5 w-5 text-lg"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
x-cloak
|
||||
class="absolute right-0 top-full mt-2 w-48 bg-background border rounded-md shadow-lg z-50"
|
||||
>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex items-center justify-start gap-2 p-3">
|
||||
<div class="flex flex-col space-y-1 leading-none">
|
||||
<p class="font-medium">{{ user.get_full_name|default:user.username }}</p>
|
||||
<p class="w-[180px] truncate text-sm text-muted-foreground">{{ user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t"></div>
|
||||
<a href="{% url 'profile' user.username %}" class="flex items-center px-3 py-2 text-sm hover:bg-accent" @click="open = false">
|
||||
<i class="fas fa-user mr-2 h-4 w-4"></i>
|
||||
Profile
|
||||
</a>
|
||||
<a href="{% url 'settings' %}" class="flex items-center px-3 py-2 text-sm hover:bg-accent" @click="open = false">
|
||||
<i class="fas fa-cog mr-2 h-4 w-4"></i>
|
||||
Settings
|
||||
</a>
|
||||
{% if has_moderation_access %}
|
||||
<a href="{% url 'moderation:dashboard' %}" class="flex items-center px-3 py-2 text-sm hover:bg-accent" @click="open = false">
|
||||
<i class="fas fa-shield-alt mr-2 h-4 w-4"></i>
|
||||
Moderation
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="border-t"></div>
|
||||
<form method="post" action="{% url 'account_logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="flex items-center w-full px-3 py-2 text-sm text-red-600 hover:bg-accent">
|
||||
<i class="fas fa-sign-out-alt mr-2 h-4 w-4"></i>
|
||||
Log out
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="p-2">
|
||||
<button
|
||||
@click="window.authModal.show('login'); open = false"
|
||||
class="flex items-center w-full px-3 py-2 text-sm hover:bg-accent rounded-md"
|
||||
>
|
||||
<i class="fas fa-sign-in-alt mr-2 h-4 w-4"></i>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
@click="window.authModal.show('register'); open = false"
|
||||
class="flex items-center w-full px-3 py-2 text-sm hover:bg-accent rounded-md"
|
||||
>
|
||||
<i class="fas fa-user-plus mr-2 h-4 w-4"></i>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div class="md:hidden flex items-center space-x-2 flex-shrink-0">
|
||||
<!-- Theme Toggle (Mobile) -->
|
||||
<div x-data="themeToggle">
|
||||
<button
|
||||
@click="toggleTheme()"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-10 w-10"
|
||||
>
|
||||
<i class="fas fa-sun h-6 w-6 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"></i>
|
||||
<i class="fas fa-moon absolute h-6 w-6 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile User Menu -->
|
||||
{% if user.is_authenticated %}
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||
<button @click="open = !open" class="relative h-8 w-8 rounded-full">
|
||||
{% if user.profile.avatar %}
|
||||
<img
|
||||
src="{{ user.profile.avatar.url }}"
|
||||
alt="{{ user.get_full_name|default:user.username }}"
|
||||
class="h-8 w-8 rounded-full object-cover"
|
||||
/>
|
||||
{% else %}
|
||||
<div class="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
|
||||
{{ user.get_full_name.0|default:user.username.0|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
||||
<!-- Mobile User Dropdown -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition
|
||||
x-cloak
|
||||
class="absolute right-0 mt-2 w-56 bg-background border rounded-md shadow-lg z-50"
|
||||
>
|
||||
<div class="flex items-center justify-start gap-2 p-2">
|
||||
<div class="flex flex-col space-y-1 leading-none">
|
||||
<p class="font-medium">{{ user.get_full_name|default:user.username }}</p>
|
||||
<p class="w-[200px] truncate text-sm text-muted-foreground">{{ user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t"></div>
|
||||
<form method="post" action="{% url 'account_logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="flex items-center w-full px-2 py-2 text-sm text-red-600 hover:bg-accent">
|
||||
<i class="fas fa-sign-out-alt mr-2 h-4 w-4"></i>
|
||||
Log out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center gap-1">
|
||||
<c-button
|
||||
variant="outline"
|
||||
size="default"
|
||||
hx_get="{% url 'account_login' %}"
|
||||
hx_target="body"
|
||||
hx_swap="beforeend"
|
||||
>
|
||||
Login
|
||||
</c-button>
|
||||
<c-button
|
||||
variant="default"
|
||||
size="default"
|
||||
hx_get="{% url 'account_signup' %}"
|
||||
hx_target="body"
|
||||
hx_swap="beforeend"
|
||||
>
|
||||
Join
|
||||
</c-button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div x-data="{ open: false }">
|
||||
<button
|
||||
@click="open = !open"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-10 w-10"
|
||||
>
|
||||
<i class="fas fa-bars h-5 w-5"></i>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Menu Overlay -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition-opacity ease-linear duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition-opacity ease-linear duration-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
x-cloak
|
||||
class="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
@click="open = false"
|
||||
>
|
||||
<!-- Mobile Menu Panel -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-in-out duration-300 transform"
|
||||
x-transition:enter-start="translate-x-full"
|
||||
x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transition ease-in-out duration-300 transform"
|
||||
x-transition:leave-start="translate-x-0"
|
||||
x-transition:leave-end="translate-x-full"
|
||||
class="fixed right-0 top-0 h-full w-full sm:w-96 bg-background border-l shadow-lg"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Mobile Menu Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-6 h-6 bg-purple-600 rounded flex items-center justify-center">
|
||||
<span class="text-white text-xs font-bold">TW</span>
|
||||
</div>
|
||||
<span class="font-bold text-lg">ThrillWiki</span>
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden lg:flex items-center space-x-8">
|
||||
<!-- Main Navigation Links -->
|
||||
<div class="flex items-center space-x-6">
|
||||
<a href="{% url 'parks:list' %}"
|
||||
class="nav-link group relative"
|
||||
hx-get="{% url 'parks:list' %}"
|
||||
hx-target="#main-content"
|
||||
hx-swap="innerHTML transition:true">
|
||||
<i class="fas fa-map-marked-alt mr-2 text-thrill-primary"></i>
|
||||
Parks
|
||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-thrill-primary to-purple-500 transition-all duration-300 group-hover:w-full"></span>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'rides:list' %}"
|
||||
class="nav-link group relative"
|
||||
hx-get="{% url 'rides:list' %}"
|
||||
hx-target="#main-content"
|
||||
hx-swap="innerHTML transition:true">
|
||||
<i class="fas fa-rocket mr-2 text-thrill-secondary"></i>
|
||||
Rides
|
||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-thrill-secondary to-red-500 transition-all duration-300 group-hover:w-full"></span>
|
||||
</a>
|
||||
|
||||
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
||||
<button class="nav-link group relative flex items-center"
|
||||
@click="open = !open">
|
||||
<i class="fas fa-compass mr-2 text-thrill-success"></i>
|
||||
Explore
|
||||
<i class="fas fa-chevron-down ml-2 text-xs transition-transform duration-200" :class="open ? 'rotate-180' : ''"></i>
|
||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-thrill-success to-teal-500 transition-all duration-300 group-hover:w-full"></span>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div x-show="open"
|
||||
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"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
class="absolute top-full left-0 mt-2 w-64 bg-white/95 dark:bg-neutral-800/95 backdrop-blur-xl rounded-2xl shadow-xl border border-neutral-200/50 dark:border-neutral-700/50 py-2"
|
||||
@click.away="open = false">
|
||||
|
||||
<a href="{% url 'parks:trending' %}"
|
||||
class="flex items-center px-4 py-3 text-sm hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<div class="w-8 h-8 bg-gradient-to-r from-thrill-primary to-purple-500 rounded-lg flex items-center justify-center mr-3">
|
||||
<i class="fas fa-fire text-white text-xs"></i>
|
||||
</div>
|
||||
<button
|
||||
@click="open = false"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-10 w-10"
|
||||
>
|
||||
<i class="fas fa-times h-5 w-5"></i>
|
||||
<div>
|
||||
<div class="font-semibold">Trending Parks</div>
|
||||
<div class="text-xs text-neutral-500">Most popular destinations</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'rides:new' %}"
|
||||
class="flex items-center px-4 py-3 text-sm hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<div class="w-8 h-8 bg-gradient-to-r from-thrill-secondary to-red-500 rounded-lg flex items-center justify-center mr-3">
|
||||
<i class="fas fa-plus text-white text-xs"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold">New Attractions</div>
|
||||
<div class="text-xs text-neutral-500">Latest additions</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'search:advanced' %}"
|
||||
class="flex items-center px-4 py-3 text-sm hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<div class="w-8 h-8 bg-gradient-to-r from-thrill-success to-teal-500 rounded-lg flex items-center justify-center mr-3">
|
||||
<i class="fas fa-search text-white text-xs"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold">Advanced Search</div>
|
||||
<div class="text-xs text-neutral-500">Find exactly what you want</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="relative" x-data="searchComponent()">
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
x-model="query"
|
||||
@input="handleInput()"
|
||||
placeholder="Search parks, rides, locations..."
|
||||
class="w-80 pl-12 pr-4 py-3 bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm border border-neutral-300/50 dark:border-neutral-600/50 rounded-xl text-sm transition-all duration-300 focus:w-96 focus:bg-white dark:focus:bg-neutral-800 focus:border-thrill-primary focus:ring-2 focus:ring-thrill-primary/20 focus:shadow-lg"
|
||||
hx-get="{% url 'parks:search_parks' %}"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#search-results"
|
||||
hx-include="this"
|
||||
hx-vals='{"quick_search": "true"}'
|
||||
name="search">
|
||||
<div class="absolute left-4 top-1/2 transform -translate-y-1/2">
|
||||
<i class="fas fa-search text-neutral-400 transition-colors duration-300"
|
||||
:class="loading ? 'fa-spinner fa-spin text-thrill-primary' : 'text-thrill-primary'"></i>
|
||||
</div>
|
||||
<button x-show="query.length > 0"
|
||||
@click="clearSearch()"
|
||||
class="absolute right-4 top-1/2 transform -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Results Dropdown -->
|
||||
<div id="search-results"
|
||||
x-show="showResults"
|
||||
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"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
class="absolute top-full left-0 right-0 mt-2 bg-white/95 dark:bg-neutral-800/95 backdrop-blur-xl rounded-2xl shadow-xl border border-neutral-200/50 dark:border-neutral-700/50 max-h-96 overflow-y-auto z-50"
|
||||
@click.away="showResults = false">
|
||||
<!-- Dynamic search results will be loaded here via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Theme Toggle -->
|
||||
<button class="p-2 rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50 transition-colors"
|
||||
onclick="toggleTheme()"
|
||||
aria-label="Toggle theme">
|
||||
<i class="fas fa-moon dark:hidden text-neutral-600"></i>
|
||||
<i class="fas fa-sun hidden dark:block text-yellow-400"></i>
|
||||
</button>
|
||||
|
||||
<!-- Notifications -->
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<button class="p-2 rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50 transition-colors relative"
|
||||
@click="open = !open"
|
||||
aria-label="Notifications">
|
||||
<i class="fas fa-bell text-neutral-600 dark:text-neutral-400"></i>
|
||||
<span class="absolute -top-1 -right-1 w-3 h-3 bg-thrill-danger rounded-full animate-pulse"></span>
|
||||
</button>
|
||||
|
||||
<!-- Notifications Dropdown -->
|
||||
<div x-show="open"
|
||||
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"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
class="absolute top-full right-0 mt-2 w-80 bg-white/95 dark:bg-neutral-800/95 backdrop-blur-xl rounded-2xl shadow-xl border border-neutral-200/50 dark:border-neutral-700/50 py-4"
|
||||
@click.away="open = false">
|
||||
|
||||
<div class="px-4 pb-2 border-b border-neutral-200/50 dark:border-neutral-700/50">
|
||||
<h3 class="font-semibold text-lg">Notifications</h3>
|
||||
</div>
|
||||
|
||||
<div class="py-2 max-h-64 overflow-y-auto">
|
||||
<div class="px-4 py-3 hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="w-2 h-2 bg-thrill-primary rounded-full mt-2 flex-shrink-0"></div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium">New park added: Universal Epic Universe</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">2 hours ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="w-2 h-2 bg-thrill-secondary rounded-full mt-2 flex-shrink-0"></div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium">Ride update: Steel Vengeance reopened</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">1 day ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pt-2 border-t border-neutral-200/50 dark:border-neutral-700/50">
|
||||
<button class="text-sm text-thrill-primary hover:text-thrill-primary-dark transition-colors">
|
||||
View all notifications
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Content -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Navigate through the ultimate theme park database
|
||||
</p>
|
||||
|
||||
<!-- Navigation Section -->
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||
NAVIGATION
|
||||
</h3>
|
||||
<div class="space-y-1">
|
||||
<a href="{% url 'home' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-home w-4 h-4"></i>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a href="{% url 'search:search' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-search w-4 h-4"></i>
|
||||
<span>Search</span>
|
||||
</a>
|
||||
<a href="{% url 'parks:park_list' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-map-marker-alt w-4 h-4"></i>
|
||||
<span>Parks</span>
|
||||
</a>
|
||||
<a href="{% url 'rides:global_ride_list' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-rocket w-4 h-4"></i>
|
||||
<span>Rides</span>
|
||||
</a>
|
||||
<a href="{% url 'rides:manufacturer_list' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-wrench w-4 h-4"></i>
|
||||
<span>Manufacturers</span>
|
||||
</a>
|
||||
<a href="{% url 'parks:operator_list' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-building w-4 h-4"></i>
|
||||
<span>Operators</span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- User Menu -->
|
||||
{% if user.is_authenticated %}
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<button class="flex items-center space-x-2 p-2 rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50 transition-colors"
|
||||
@click="open = !open">
|
||||
{% if user.avatar %}
|
||||
<img src="{{ user.avatar.url }}" alt="{{ user.username }}" class="w-8 h-8 rounded-full object-cover">
|
||||
{% else %}
|
||||
<div class="w-8 h-8 bg-gradient-to-r from-thrill-primary to-purple-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white text-sm font-semibold">{{ user.username|first|upper }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<i class="fas fa-chevron-down text-xs transition-transform duration-200" :class="open ? 'rotate-180' : ''"></i>
|
||||
</button>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<div x-show="open"
|
||||
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"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
class="absolute top-full right-0 mt-2 w-64 bg-white/95 dark:bg-neutral-800/95 backdrop-blur-xl rounded-2xl shadow-xl border border-neutral-200/50 dark:border-neutral-700/50 py-2"
|
||||
@click.away="open = false">
|
||||
|
||||
<div class="px-4 py-3 border-b border-neutral-200/50 dark:border-neutral-700/50">
|
||||
<p class="font-semibold">{{ user.get_full_name|default:user.username }}</p>
|
||||
<p class="text-sm text-neutral-500">{{ user.email }}</p>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'accounts:profile' %}" class="flex items-center px-4 py-3 text-sm hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<i class="fas fa-user mr-3 text-thrill-primary"></i>
|
||||
Profile
|
||||
</a>
|
||||
|
||||
<a href="{% url 'accounts:settings' %}" class="flex items-center px-4 py-3 text-sm hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<i class="fas fa-cog mr-3 text-neutral-500"></i>
|
||||
Settings
|
||||
</a>
|
||||
|
||||
<a href="{% url 'accounts:favorites' %}" class="flex items-center px-4 py-3 text-sm hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<i class="fas fa-heart mr-3 text-red-500"></i>
|
||||
Favorites
|
||||
</a>
|
||||
|
||||
<div class="border-t border-neutral-200/50 dark:border-neutral-700/50 mt-2 pt-2">
|
||||
<form method="post" action="{% url 'accounts:logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="flex items-center w-full px-4 py-3 text-sm text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">
|
||||
<i class="fas fa-sign-out-alt mr-3"></i>
|
||||
Sign Out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center space-x-3">
|
||||
<button class="btn-ghost btn-sm"
|
||||
onclick="openAuthModal('login')">
|
||||
Sign In
|
||||
</button>
|
||||
<button class="btn-primary btn-sm"
|
||||
onclick="openAuthModal('register')">
|
||||
Join Now
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Search Bar -->
|
||||
<div class="md:hidden border-t bg-background">
|
||||
<div class="px-4 py-3">
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<div class="relative flex-1">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"></i>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search parks, rides..."
|
||||
class="w-full pl-10 pr-3 h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
hx-get="{% url 'search:search' %}"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
hx-target="#mobile-search-results"
|
||||
hx-include="this"
|
||||
name="q"
|
||||
/>
|
||||
<!-- Mobile Menu Button -->
|
||||
<button class="lg:hidden p-2 rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50 transition-colors"
|
||||
@click="isOpen = !isOpen"
|
||||
aria-label="Toggle mobile menu">
|
||||
<div class="w-6 h-6 relative">
|
||||
<span class="absolute top-1 left-0 w-6 h-0.5 bg-current transition-all duration-300"
|
||||
:class="isOpen ? 'rotate-45 top-2.5' : ''"></span>
|
||||
<span class="absolute top-2.5 left-0 w-6 h-0.5 bg-current transition-all duration-300"
|
||||
:class="isOpen ? 'opacity-0' : ''"></span>
|
||||
<span class="absolute top-4 left-0 w-6 h-0.5 bg-current transition-all duration-300"
|
||||
:class="isOpen ? '-rotate-45 top-2.5' : ''"></span>
|
||||
</div>
|
||||
<button type="submit" class="flex-shrink-0 inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 w-10">
|
||||
<i class="fas fa-search h-4 w-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="mobile-search-results" class="mt-2"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation Menu -->
|
||||
<div x-show="isOpen"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 transform -translate-y-4"
|
||||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 transform translate-y-0"
|
||||
x-transition:leave-end="opacity-0 transform -translate-y-4"
|
||||
class="lg:hidden mt-6 bg-white/95 dark:bg-neutral-800/95 backdrop-blur-xl rounded-2xl border border-neutral-200/50 dark:border-neutral-700/50 p-6">
|
||||
|
||||
<!-- Mobile Search -->
|
||||
<div class="mb-6" x-data="searchComponent()">
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
x-model="query"
|
||||
@input="handleInput()"
|
||||
placeholder="Search parks, rides, locations..."
|
||||
class="w-full pl-12 pr-4 py-3 bg-neutral-100/50 dark:bg-neutral-700/50 border border-neutral-300/50 dark:border-neutral-600/50 rounded-xl text-sm"
|
||||
hx-get="{% url 'parks:search_parks' %}"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#mobile-search-results"
|
||||
hx-include="this"
|
||||
hx-vals='{"quick_search": "true"}'
|
||||
name="search">
|
||||
<div class="absolute left-4 top-1/2 transform -translate-y-1/2">
|
||||
<i class="fas fa-search text-neutral-400 transition-colors duration-300"
|
||||
:class="loading ? 'fa-spinner fa-spin text-thrill-primary' : ''"></i>
|
||||
</div>
|
||||
<button x-show="query.length > 0"
|
||||
@click="clearSearch()"
|
||||
class="absolute right-4 top-1/2 transform -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Search Results -->
|
||||
<div id="mobile-search-results"
|
||||
x-show="showResults"
|
||||
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"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
class="mt-2 bg-white/95 dark:bg-neutral-800/95 backdrop-blur-xl rounded-2xl shadow-xl border border-neutral-200/50 dark:border-neutral-700/50 max-h-64 overflow-y-auto"
|
||||
@click.away="showResults = false">
|
||||
<!-- Dynamic search results will be loaded here via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation Links -->
|
||||
<div class="space-y-4">
|
||||
<a href="{% url 'parks:list' %}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors"
|
||||
@click="isOpen = false">
|
||||
<i class="fas fa-map-marked-alt mr-3 text-thrill-primary"></i>
|
||||
<span class="font-medium">Parks</span>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'rides:list' %}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors"
|
||||
@click="isOpen = false">
|
||||
<i class="fas fa-rocket mr-3 text-thrill-secondary"></i>
|
||||
<span class="font-medium">Rides</span>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'search:advanced' %}"
|
||||
class="flex items-center p-3 rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors"
|
||||
@click="isOpen = false">
|
||||
<i class="fas fa-search mr-3 text-thrill-success"></i>
|
||||
<span class="font-medium">Advanced Search</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile User Actions -->
|
||||
<div class="mt-6 pt-6 border-t border-neutral-200/50 dark:border-neutral-700/50">
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex items-center space-x-3 mb-4">
|
||||
{% if user.avatar %}
|
||||
<img src="{{ user.avatar.url }}" alt="{{ user.username }}" class="w-10 h-10 rounded-full object-cover">
|
||||
{% else %}
|
||||
<div class="w-10 h-10 bg-gradient-to-r from-thrill-primary to-purple-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white font-semibold">{{ user.username|first|upper }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<p class="font-semibold">{{ user.get_full_name|default:user.username }}</p>
|
||||
<p class="text-sm text-neutral-500">{{ user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<a href="{% url 'accounts:profile' %}" class="flex items-center p-2 rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<i class="fas fa-user mr-3 text-thrill-primary"></i>
|
||||
Profile
|
||||
</a>
|
||||
<a href="{% url 'accounts:settings' %}" class="flex items-center p-2 rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-700/50 transition-colors">
|
||||
<i class="fas fa-cog mr-3 text-neutral-500"></i>
|
||||
Settings
|
||||
</a>
|
||||
<form method="post" action="{% url 'accounts:logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="flex items-center w-full p-2 rounded-lg text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">
|
||||
<i class="fas fa-sign-out-alt mr-3"></i>
|
||||
Sign Out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="space-y-3">
|
||||
<button class="btn-primary w-full" onclick="openAuthModal('register')">
|
||||
Join ThrillWiki
|
||||
</button>
|
||||
<button class="btn-secondary w-full" onclick="openAuthModal('login')">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Spacer to prevent content from hiding behind fixed header -->
|
||||
<div class="h-20"></div>
|
||||
|
||||
@@ -1,169 +1,379 @@
|
||||
{% extends 'base/base.html' %}
|
||||
{% load static %}
|
||||
{% load cotton %}
|
||||
|
||||
{% block title %}ThrillWiki - Theme Parks & Attractions Guide{% endblock %}
|
||||
{% block title %}ThrillWiki - Your Ultimate Theme Park Adventure Guide{% endblock %}
|
||||
|
||||
{% block meta_description %}Discover the world's best theme parks and thrilling rides. Explore amazing parks, find detailed ride information, and share your adventures with fellow theme park enthusiasts.{% endblock %}
|
||||
|
||||
{% block meta_keywords %}theme parks, roller coasters, attractions, rides, amusement parks, Disney World, Universal Studios, Cedar Point, Six Flags, thrill rides{% endblock %}
|
||||
|
||||
{% block og_title %}ThrillWiki - Your Ultimate Theme Park & Attractions Guide{% endblock %}
|
||||
{% block og_description %}Discover the world's best theme parks and thrilling rides. Explore amazing parks, find detailed ride information, and share your adventures with fellow theme park enthusiasts.{% endblock %}
|
||||
{% block og_type %}website{% endblock %}
|
||||
|
||||
{% block twitter_title %}ThrillWiki - Your Ultimate Theme Park & Attractions Guide{% endblock %}
|
||||
{% block twitter_description %}Discover the world's best theme parks and thrilling rides. Explore amazing parks, find detailed ride information, and share your adventures.{% endblock %}
|
||||
|
||||
{% block structured_data %}
|
||||
<script type="application/ld+json" nonce="{{ request.csp_nonce }}">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "ThrillWiki",
|
||||
"description": "Your ultimate guide to theme parks and attractions worldwide",
|
||||
"url": "{{ request.scheme }}://{{ request.get_host }}",
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": {
|
||||
"@type": "EntryPoint",
|
||||
"urlTemplate": "{{ request.scheme }}://{{ request.get_host }}/search/?q={search_term_string}"
|
||||
},
|
||||
"query-input": "required name=search_term_string"
|
||||
},
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "ThrillWiki",
|
||||
"description": "The ultimate theme park and attractions database"
|
||||
},
|
||||
"mainEntity": {
|
||||
"@type": "ItemList",
|
||||
"name": "Featured Theme Parks and Attractions",
|
||||
"description": "Top-rated theme parks and thrilling rides from around the world"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block meta_description %}Discover the world's most thrilling theme parks and attractions. Explore detailed guides, stunning photos, and insider tips for your next adventure at Disney, Universal, Cedar Point, and beyond.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Hero Section -->
|
||||
<div class="mb-12 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="px-4 py-12 text-center">
|
||||
<h1 class="mb-6 text-4xl font-bold text-gray-900 md:text-5xl lg:text-6xl dark:text-white">
|
||||
Welcome to ThrillWiki
|
||||
</h1>
|
||||
<p class="max-w-3xl mx-auto mb-8 text-xl text-gray-600 md:text-2xl dark:text-gray-300">
|
||||
Your ultimate guide to theme parks and attractions worldwide
|
||||
<!-- Hero Section - Absolutely Stunning -->
|
||||
<section class="hero relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||
<!-- Animated Background Elements -->
|
||||
<div class="absolute inset-0 opacity-10 dark:opacity-5">
|
||||
<div class="absolute top-20 left-10 w-32 h-32 bg-thrill-primary rounded-full blur-3xl animate-pulse"></div>
|
||||
<div class="absolute top-40 right-20 w-48 h-48 bg-thrill-secondary rounded-full blur-3xl animate-pulse" style="animation-delay: 1s;"></div>
|
||||
<div class="absolute bottom-32 left-1/4 w-40 h-40 bg-purple-500 rounded-full blur-3xl animate-pulse" style="animation-delay: 2s;"></div>
|
||||
<div class="absolute bottom-20 right-1/3 w-36 h-36 bg-pink-500 rounded-full blur-3xl animate-pulse" style="animation-delay: 0.5s;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Hero Content -->
|
||||
<div class="hero-content container mx-auto px-6 relative z-10">
|
||||
<!-- Hero Badge -->
|
||||
<div class="flex justify-center mb-8">
|
||||
<div class="badge badge-info badge-lg pulse-glow">
|
||||
<i class="fas fa-rocket mr-2"></i>
|
||||
World's #1 Theme Park Guide
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Title with Stunning Typography -->
|
||||
<h1 class="hero-title slide-in-up">
|
||||
Discover Your Next
|
||||
<span class="block">Thrilling Adventure</span>
|
||||
</h1>
|
||||
|
||||
<!-- Hero Subtitle -->
|
||||
<p class="hero-subtitle slide-in-up" style="animation-delay: 0.2s;">
|
||||
Explore the world's most incredible theme parks, from heart-pounding roller coasters to magical experiences.
|
||||
Your ultimate adventure starts here with insider guides, stunning visuals, and expert recommendations.
|
||||
</p>
|
||||
|
||||
<!-- Hero CTA Buttons -->
|
||||
<div class="hero-cta slide-in-up" style="animation-delay: 0.4s;">
|
||||
<button class="btn-primary btn-lg hover-lift"
|
||||
hx-get="/parks/"
|
||||
hx-target="#main-content"
|
||||
hx-swap="innerHTML transition:true">
|
||||
<i class="fas fa-map-marked-alt mr-3"></i>
|
||||
Explore Parks
|
||||
</button>
|
||||
<button class="btn-secondary btn-lg hover-lift">
|
||||
<i class="fas fa-play mr-3"></i>
|
||||
Watch Adventure
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hero Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-8 mt-16 slide-in-up" style="animation-delay: 0.6s;">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl md:text-4xl font-bold text-thrill-primary mb-2">500+</div>
|
||||
<div class="text-neutral-600 dark:text-neutral-400">Theme Parks</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl md:text-4xl font-bold text-thrill-secondary mb-2">2,000+</div>
|
||||
<div class="text-neutral-600 dark:text-neutral-400">Thrilling Rides</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl md:text-4xl font-bold text-thrill-success mb-2">50K+</div>
|
||||
<div class="text-neutral-600 dark:text-neutral-400">Happy Visitors</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl md:text-4xl font-bold text-purple-500 mb-2">100+</div>
|
||||
<div class="text-neutral-600 dark:text-neutral-400">Countries</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll Indicator -->
|
||||
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
|
||||
<div class="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center">
|
||||
<div class="w-1 h-3 bg-white/50 rounded-full mt-2 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Parks Section -->
|
||||
<section class="py-24 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm">
|
||||
<div class="container mx-auto px-6">
|
||||
<!-- Section Header -->
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl md:text-5xl font-bold mb-6">
|
||||
<span class="bg-gradient-to-r from-thrill-primary to-thrill-secondary bg-clip-text text-transparent">
|
||||
Featured Destinations
|
||||
</span>
|
||||
</h2>
|
||||
<p class="text-xl text-neutral-600 dark:text-neutral-400 max-w-3xl mx-auto">
|
||||
Discover the world's most incredible theme parks, each offering unique thrills and unforgettable memories
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Featured Parks Grid -->
|
||||
<div class="grid-auto-fit-lg"
|
||||
hx-get="/api/parks/featured/"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="innerHTML">
|
||||
<!-- Loading Skeletons -->
|
||||
<div class="card hover-lift">
|
||||
<div class="loading-skeleton aspect-video rounded-t-2xl"></div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="loading-skeleton h-6 w-3/4 rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-full rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-2/3 rounded"></div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="loading-skeleton h-6 w-20 rounded-full"></div>
|
||||
<div class="loading-skeleton h-8 w-24 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card hover-lift">
|
||||
<div class="loading-skeleton aspect-video rounded-t-2xl"></div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="loading-skeleton h-6 w-3/4 rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-full rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-2/3 rounded"></div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="loading-skeleton h-6 w-20 rounded-full"></div>
|
||||
<div class="loading-skeleton h-8 w-24 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card hover-lift">
|
||||
<div class="loading-skeleton aspect-video rounded-t-2xl"></div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="loading-skeleton h-6 w-3/4 rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-full rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-2/3 rounded"></div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="loading-skeleton h-6 w-20 rounded-full"></div>
|
||||
<div class="loading-skeleton h-8 w-24 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Trending Rides Section -->
|
||||
<section class="py-24">
|
||||
<div class="container mx-auto px-6">
|
||||
<!-- Section Header -->
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl md:text-5xl font-bold mb-6">
|
||||
<span class="bg-gradient-to-r from-thrill-secondary to-red-500 bg-clip-text text-transparent">
|
||||
Trending Thrills
|
||||
</span>
|
||||
</h2>
|
||||
<p class="text-xl text-neutral-600 dark:text-neutral-400 max-w-3xl mx-auto">
|
||||
The hottest rides everyone's talking about - from record-breaking coasters to innovative attractions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Trending Rides Carousel -->
|
||||
<div class="relative">
|
||||
<div class="grid-auto-fit-md"
|
||||
hx-get="/api/rides/trending/"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="innerHTML">
|
||||
<!-- Loading Skeletons for Rides -->
|
||||
<div class="card-ride hover-lift">
|
||||
<div class="loading-skeleton aspect-square rounded-t-2xl"></div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="loading-skeleton h-5 w-3/4 rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-full rounded"></div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="loading-skeleton h-5 w-16 rounded-full"></div>
|
||||
<div class="loading-skeleton h-6 w-20 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-ride hover-lift">
|
||||
<div class="loading-skeleton aspect-square rounded-t-2xl"></div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="loading-skeleton h-5 w-3/4 rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-full rounded"></div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="loading-skeleton h-5 w-16 rounded-full"></div>
|
||||
<div class="loading-skeleton h-6 w-20 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-ride hover-lift">
|
||||
<div class="loading-skeleton aspect-square rounded-t-2xl"></div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="loading-skeleton h-5 w-3/4 rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-full rounded"></div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="loading-skeleton h-5 w-16 rounded-full"></div>
|
||||
<div class="loading-skeleton h-6 w-20 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-ride hover-lift">
|
||||
<div class="loading-skeleton aspect-square rounded-t-2xl"></div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="loading-skeleton h-5 w-3/4 rounded"></div>
|
||||
<div class="loading-skeleton h-4 w-full rounded"></div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="loading-skeleton h-5 w-16 rounded-full"></div>
|
||||
<div class="loading-skeleton h-6 w-20 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="py-24 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm">
|
||||
<div class="container mx-auto px-6">
|
||||
<!-- Section Header -->
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl md:text-5xl font-bold mb-6">
|
||||
<span class="bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
Why Choose ThrillWiki?
|
||||
</span>
|
||||
</h2>
|
||||
<p class="text-xl text-neutral-600 dark:text-neutral-400 max-w-3xl mx-auto">
|
||||
We're more than just a guide - we're your adventure companion
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<!-- Feature 1 -->
|
||||
<div class="card-feature hover-lift text-center">
|
||||
<div class="w-16 h-16 bg-gradient-to-r from-thrill-primary to-purple-500 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<i class="fas fa-map-marked-alt text-2xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-4">Comprehensive Guides</h3>
|
||||
<p class="text-neutral-600 dark:text-neutral-400">
|
||||
Detailed information on every park, ride, and attraction with insider tips and expert recommendations.
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<a href="{% url 'parks:park_list' %}"
|
||||
class="px-8 py-3 text-lg btn-primary">
|
||||
Explore Parks
|
||||
</a>
|
||||
<a href="{% url 'rides:global_ride_list' %}"
|
||||
class="px-8 py-3 text-lg btn-secondary">
|
||||
View Rides
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2 -->
|
||||
<div class="card-feature hover-lift text-center">
|
||||
<div class="w-16 h-16 bg-gradient-to-r from-thrill-secondary to-red-500 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<i class="fas fa-camera text-2xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-4">Stunning Visuals</h3>
|
||||
<p class="text-neutral-600 dark:text-neutral-400">
|
||||
High-quality photos and videos that bring the magic to life before you even arrive.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 3 -->
|
||||
<div class="card-feature hover-lift text-center">
|
||||
<div class="w-16 h-16 bg-gradient-to-r from-thrill-success to-teal-500 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<i class="fas fa-users text-2xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-4">Community Driven</h3>
|
||||
<p class="text-neutral-600 dark:text-neutral-400">
|
||||
Real reviews and experiences from fellow thrill-seekers and theme park enthusiasts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 4 -->
|
||||
<div class="card-feature hover-lift text-center">
|
||||
<div class="w-16 h-16 bg-gradient-to-r from-pink-500 to-rose-500 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<i class="fas fa-mobile-alt text-2xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-4">Mobile Optimized</h3>
|
||||
<p class="text-neutral-600 dark:text-neutral-400">
|
||||
Perfect experience on any device, from planning at home to navigating in the park.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 5 -->
|
||||
<div class="card-feature hover-lift text-center">
|
||||
<div class="w-16 h-16 bg-gradient-to-r from-indigo-500 to-blue-500 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<i class="fas fa-clock text-2xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-4">Real-Time Updates</h3>
|
||||
<p class="text-neutral-600 dark:text-neutral-400">
|
||||
Live wait times, operating hours, and park status to help you make the most of your visit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 6 -->
|
||||
<div class="card-feature hover-lift text-center">
|
||||
<div class="w-16 h-16 bg-gradient-to-r from-yellow-500 to-orange-500 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<i class="fas fa-star text-2xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-4">Expert Reviews</h3>
|
||||
<p class="text-neutral-600 dark:text-neutral-400">
|
||||
Professional insights and ratings from theme park experts and industry professionals.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<div class="grid-adaptive-sm mb-12">
|
||||
<!-- Total Parks -->
|
||||
<a href="{% url 'parks:park_list' %}"
|
||||
class="flex flex-col items-center justify-center p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1 hover:shadow-xl">
|
||||
<div class="mb-2 text-4xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{{ stats.total_parks }}
|
||||
</div>
|
||||
<div class="text-xl text-gray-600 dark:text-gray-300">
|
||||
Theme Parks
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Total Attractions -->
|
||||
<a href="{% url 'rides:global_ride_list' %}"
|
||||
class="flex flex-col items-center justify-center p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1 hover:shadow-xl">
|
||||
<div class="mb-2 text-4xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{{ stats.ride_count }}
|
||||
</div>
|
||||
<div class="text-xl text-gray-600 dark:text-gray-300">
|
||||
Attractions
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Total Roller Coasters -->
|
||||
<a href="{% url 'rides:global_roller_coasters' %}"
|
||||
class="flex flex-col items-center justify-center p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1 hover:shadow-xl">
|
||||
<div class="mb-2 text-4xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{{ stats.coaster_count }}
|
||||
</div>
|
||||
<div class="text-xl text-gray-600 dark:text-gray-300">
|
||||
Roller Coasters
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Featured Content -->
|
||||
<div class="grid-adaptive">
|
||||
<!-- Trending Parks -->
|
||||
<div class="p-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
|
||||
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Trending Parks
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
{% for park in popular_parks %}
|
||||
<c-park_card :park="park" view_mode="grid" />
|
||||
{% empty %}
|
||||
<div class="flex flex-col items-center justify-center h-48 p-8 text-center bg-gray-50 rounded-lg dark:bg-gray-800/50">
|
||||
<div class="mb-4 text-4xl">🎢</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">No Parks Yet</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Parks will appear here once they're added to the database</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- Call to Action Section -->
|
||||
<section class="py-24 relative overflow-hidden">
|
||||
<!-- Background Gradient -->
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-thrill-primary via-purple-600 to-pink-600"></div>
|
||||
<div class="absolute inset-0 bg-black/20"></div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="container mx-auto px-6 relative z-10 text-center text-white">
|
||||
<h2 class="text-4xl md:text-6xl font-bold mb-6">
|
||||
Ready for Your Next Adventure?
|
||||
</h2>
|
||||
<p class="text-xl md:text-2xl mb-12 max-w-3xl mx-auto opacity-90">
|
||||
Join thousands of thrill-seekers who trust ThrillWiki to plan their perfect theme park adventures
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-6 justify-center items-center">
|
||||
<button class="btn-secondary btn-lg bg-white text-thrill-primary hover:bg-white/90 hover-lift">
|
||||
<i class="fas fa-user-plus mr-3"></i>
|
||||
Join the Community
|
||||
</button>
|
||||
<button class="btn-ghost btn-lg text-white border-white/30 hover:bg-white/10 hover-lift">
|
||||
<i class="fas fa-compass mr-3"></i>
|
||||
Start Exploring
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Trending Rides -->
|
||||
<div class="p-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
|
||||
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Trending Rides
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
{% for ride in popular_rides %}
|
||||
<c-ride_card :ride="ride" url_variant="park" />
|
||||
{% empty %}
|
||||
<div class="flex flex-col items-center justify-center h-48 p-8 text-center bg-gray-50 rounded-lg dark:bg-gray-800/50">
|
||||
<div class="mb-4 text-4xl">🎠</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">No Rides Yet</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Rides will appear here once they're added to the database</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Highest Rated -->
|
||||
<div class="p-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
|
||||
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Highest Rated
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
{% for item in highest_rated %}
|
||||
{% if item.park %}
|
||||
<!-- This is a ride -->
|
||||
<c-ride_card :ride="item" url_variant="park" />
|
||||
{% else %}
|
||||
<!-- This is a park -->
|
||||
<c-park_card :park="item" view_mode="grid" />
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
<div class="flex flex-col items-center justify-center h-48 p-8 text-center bg-gray-50 rounded-lg dark:bg-gray-800/50">
|
||||
<div class="mb-4 text-4xl">⭐</div>
|
||||
<div class="text-lg font-medium text-gray-900 dark:text-white">No Ratings Yet</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Highest rated content will appear here once users start rating</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Enhanced JavaScript for Interactions -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Enable HTMX view transitions globally
|
||||
htmx.config.globalViewTransitions = true;
|
||||
|
||||
// Add staggered animations to elements
|
||||
const animatedElements = document.querySelectorAll('.slide-in-up');
|
||||
animatedElements.forEach((el, index) => {
|
||||
el.style.animationDelay = `${index * 0.1}s`;
|
||||
});
|
||||
|
||||
// Parallax effect for hero background elements
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrolled = window.pageYOffset;
|
||||
const parallaxElements = document.querySelectorAll('.hero .absolute');
|
||||
|
||||
parallaxElements.forEach((el, index) => {
|
||||
const speed = 0.5 + (index * 0.1);
|
||||
el.style.transform = `translateY(${scrolled * speed}px)`;
|
||||
});
|
||||
});
|
||||
|
||||
// Intersection Observer for reveal animations
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('fade-in');
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe all cards for reveal animations
|
||||
document.querySelectorAll('.card, .card-feature, .card-park, .card-ride').forEach(card => {
|
||||
observer.observe(card);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -225,19 +225,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
maps[submissionId].setView([0, 0], 2);
|
||||
}
|
||||
|
||||
// Handle map clicks
|
||||
maps[submissionId].on('click', async function(e) {
|
||||
// Handle map clicks - HTMX version
|
||||
maps[submissionId].on('click', function(e) {
|
||||
try {
|
||||
const normalized = validateCoordinates(e.latlng.lat, e.latlng.lng);
|
||||
const response = await fetch(`/parks/search/reverse-geocode/?lat=${normalized.lat}&lon=${normalized.lng}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Geocoding request failed');
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
updateLocation(normalized.lat, normalized.lng, data);
|
||||
|
||||
// Create a temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.style.display = 'none';
|
||||
tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/');
|
||||
tempForm.setAttribute('hx-vals', JSON.stringify({
|
||||
lat: normalized.lat,
|
||||
lon: normalized.lng
|
||||
}));
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add event listener for HTMX response
|
||||
tempForm.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.successful) {
|
||||
try {
|
||||
const data = JSON.parse(event.detail.xhr.responseText);
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
updateLocation(normalized.lat, normalized.lng, data);
|
||||
} catch (error) {
|
||||
console.error('Location update failed:', error);
|
||||
alert(error.message || 'Failed to update location. Please try again.');
|
||||
}
|
||||
} else {
|
||||
console.error('Geocoding request failed');
|
||||
alert('Failed to update location. Please try again.');
|
||||
}
|
||||
// Clean up temporary form
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Location update failed:', error);
|
||||
alert(error.message || 'Failed to update location. Please try again.');
|
||||
@@ -326,7 +353,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle location search
|
||||
// Handle location search - HTMX version
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
const query = this.value.trim();
|
||||
@@ -336,48 +363,68 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(async function() {
|
||||
try {
|
||||
const response = await fetch(`/parks/search/location/?q=${encodeURIComponent(query)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Search request failed');
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
const resultsHtml = data.results.map((result, index) => `
|
||||
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
|
||||
data-result-index="${index}">
|
||||
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
${(result.address && result.address.city) ? result.address.city + ', ' : ''}${(result.address && result.address.country) || ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
searchResults.innerHTML = resultsHtml;
|
||||
searchResults.classList.remove('hidden');
|
||||
|
||||
// Store results data
|
||||
searchResults.dataset.results = JSON.stringify(data.results);
|
||||
|
||||
// Add click handlers
|
||||
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
|
||||
el.addEventListener('click', function() {
|
||||
const results = JSON.parse(searchResults.dataset.results);
|
||||
const result = results[this.dataset.resultIndex];
|
||||
selectLocation(result);
|
||||
});
|
||||
});
|
||||
searchTimeout = setTimeout(function() {
|
||||
// Create a temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.style.display = 'none';
|
||||
tempForm.setAttribute('hx-get', '/parks/search/location/');
|
||||
tempForm.setAttribute('hx-vals', JSON.stringify({
|
||||
q: query
|
||||
}));
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add event listener for HTMX response
|
||||
tempForm.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.successful) {
|
||||
try {
|
||||
const data = JSON.parse(event.detail.xhr.responseText);
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
const resultsHtml = data.results.map((result, index) => `
|
||||
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
|
||||
data-result-index="${index}">
|
||||
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
${(result.address && result.address.city) ? result.address.city + ', ' : ''}${(result.address && result.address.country) || ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
searchResults.innerHTML = resultsHtml;
|
||||
searchResults.classList.remove('hidden');
|
||||
|
||||
// Store results data
|
||||
searchResults.dataset.results = JSON.stringify(data.results);
|
||||
|
||||
// Add click handlers
|
||||
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
|
||||
el.addEventListener('click', function() {
|
||||
const results = JSON.parse(searchResults.dataset.results);
|
||||
const result = results[this.dataset.resultIndex];
|
||||
selectLocation(result);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
|
||||
console.error('Search request failed');
|
||||
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
// Clean up temporary form
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
}, 300);
|
||||
});
|
||||
|
||||
|
||||
@@ -176,19 +176,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle map clicks
|
||||
map.on('click', async function(e) {
|
||||
// Handle map clicks - HTMX version
|
||||
map.on('click', function(e) {
|
||||
try {
|
||||
const normalized = validateCoordinates(e.latlng.lat, e.latlng.lng);
|
||||
const response = await fetch(`/parks/search/reverse-geocode/?lat=${normalized.lat}&lon=${normalized.lng}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Geocoding request failed');
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
updateLocation(normalized.lat, normalized.lng, data);
|
||||
|
||||
// Create a temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.style.display = 'none';
|
||||
tempForm.setAttribute('hx-get', '/parks/search/reverse-geocode/');
|
||||
tempForm.setAttribute('hx-vals', JSON.stringify({
|
||||
lat: normalized.lat,
|
||||
lon: normalized.lng
|
||||
}));
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add event listener for HTMX response
|
||||
tempForm.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.successful) {
|
||||
try {
|
||||
const data = JSON.parse(event.detail.xhr.responseText);
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
updateLocation(normalized.lat, normalized.lng, data);
|
||||
} catch (error) {
|
||||
console.error('Location update failed:', error);
|
||||
alert(error.message || 'Failed to update location. Please try again.');
|
||||
}
|
||||
} else {
|
||||
console.error('Geocoding request failed');
|
||||
alert('Failed to update location. Please try again.');
|
||||
}
|
||||
// Clean up temporary form
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Location update failed:', error);
|
||||
alert(error.message || 'Failed to update location. Please try again.');
|
||||
@@ -199,7 +226,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize map
|
||||
initMap();
|
||||
|
||||
// Handle location search
|
||||
// Handle location search - HTMX version
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
const query = this.value.trim();
|
||||
@@ -209,54 +236,74 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(async function() {
|
||||
try {
|
||||
const response = await fetch(`/parks/search/location/?q=${encodeURIComponent(query)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Search request failed');
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
const resultsHtml = data.results.map((result, index) => `
|
||||
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
|
||||
data-result-index="${index}">
|
||||
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
${[
|
||||
result.street,
|
||||
result.city || (result.address && (result.address.city || result.address.town || result.address.village)),
|
||||
result.state || (result.address && (result.address.state || result.address.region)),
|
||||
result.country || (result.address && result.address.country),
|
||||
result.postal_code || (result.address && result.address.postcode)
|
||||
].filter(Boolean).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
searchResults.innerHTML = resultsHtml;
|
||||
searchResults.classList.remove('hidden');
|
||||
|
||||
// Store results data
|
||||
searchResults.dataset.results = JSON.stringify(data.results);
|
||||
|
||||
// Add click handlers
|
||||
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
|
||||
el.addEventListener('click', function() {
|
||||
const results = JSON.parse(searchResults.dataset.results);
|
||||
const result = results[this.dataset.resultIndex];
|
||||
selectLocation(result);
|
||||
});
|
||||
});
|
||||
searchTimeout = setTimeout(function() {
|
||||
// Create a temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.style.display = 'none';
|
||||
tempForm.setAttribute('hx-get', '/parks/search/location/');
|
||||
tempForm.setAttribute('hx-vals', JSON.stringify({
|
||||
q: query
|
||||
}));
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add event listener for HTMX response
|
||||
tempForm.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.successful) {
|
||||
try {
|
||||
const data = JSON.parse(event.detail.xhr.responseText);
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
const resultsHtml = data.results.map((result, index) => `
|
||||
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
|
||||
data-result-index="${index}">
|
||||
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
${[
|
||||
result.street,
|
||||
result.city || (result.address && (result.address.city || result.address.town || result.address.village)),
|
||||
result.state || (result.address && (result.address.state || result.address.region)),
|
||||
result.country || (result.address && result.address.country),
|
||||
result.postal_code || (result.address && result.address.postcode)
|
||||
].filter(Boolean).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
searchResults.innerHTML = resultsHtml;
|
||||
searchResults.classList.remove('hidden');
|
||||
|
||||
// Store results data
|
||||
searchResults.dataset.results = JSON.stringify(data.results);
|
||||
|
||||
// Add click handlers
|
||||
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
|
||||
el.addEventListener('click', function() {
|
||||
const results = JSON.parse(searchResults.dataset.results);
|
||||
const result = results[this.dataset.resultIndex];
|
||||
selectLocation(result);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
|
||||
console.error('Search request failed');
|
||||
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
// Clean up temporary form
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
}, 300);
|
||||
});
|
||||
|
||||
|
||||
490
templates/search/advanced_search.html
Normal file
490
templates/search/advanced_search.html
Normal file
@@ -0,0 +1,490 @@
|
||||
{% extends 'base/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Advanced Search - ThrillWiki{% endblock %}
|
||||
|
||||
{% block meta_description %}Find your perfect theme park adventure with our advanced search. Filter by location, thrill level, ride type, and more to discover exactly what you're looking for.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Advanced Search Page -->
|
||||
<div class="min-h-screen bg-gradient-to-br from-white via-blue-50/30 to-indigo-50/30 dark:from-gray-950 dark:via-indigo-950/30 dark:to-purple-950/30">
|
||||
|
||||
<!-- Search Header -->
|
||||
<section class="py-16 bg-gradient-to-r from-thrill-primary/10 via-purple-500/10 to-pink-500/10 backdrop-blur-sm">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="text-center max-w-4xl mx-auto">
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-6">
|
||||
<span class="bg-gradient-to-r from-thrill-primary via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
Advanced Search
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-xl text-neutral-600 dark:text-neutral-400 mb-8">
|
||||
Find your perfect theme park adventure with precision. Use our advanced filters to discover exactly what you're looking for.
|
||||
</p>
|
||||
|
||||
<!-- Quick Search Bar -->
|
||||
<div class="relative max-w-2xl mx-auto">
|
||||
<input type="text"
|
||||
id="quick-search"
|
||||
placeholder="Quick search: parks, rides, locations..."
|
||||
class="w-full pl-16 pr-6 py-4 bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm border border-neutral-300/50 dark:border-neutral-600/50 rounded-2xl text-lg shadow-lg focus:shadow-xl focus:bg-white dark:focus:bg-neutral-800 focus:border-thrill-primary focus:ring-2 focus:ring-thrill-primary/20 transition-all duration-300"
|
||||
hx-get="/search/quick/"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#quick-results">
|
||||
<div class="absolute left-6 top-1/2 transform -translate-y-1/2">
|
||||
<i class="fas fa-search text-2xl text-thrill-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Results -->
|
||||
<div id="quick-results" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Filters -->
|
||||
<section class="py-12">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="grid lg:grid-cols-4 gap-8">
|
||||
|
||||
<!-- Filters Sidebar -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="card p-6 sticky top-24">
|
||||
<h2 class="text-2xl font-bold mb-6 flex items-center">
|
||||
<i class="fas fa-filter mr-3 text-thrill-primary"></i>
|
||||
Filters
|
||||
</h2>
|
||||
|
||||
<form id="advanced-search-form"
|
||||
hx-get="/search/results/"
|
||||
hx-target="#search-results"
|
||||
hx-trigger="change, submit"
|
||||
hx-swap="innerHTML"
|
||||
class="space-y-6">
|
||||
|
||||
<!-- Search Type Toggle -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Search For</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="flex items-center p-3 border border-neutral-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-thrill-primary/5 transition-colors">
|
||||
<input type="radio" name="search_type" value="parks" checked class="sr-only">
|
||||
<div class="w-4 h-4 border-2 border-thrill-primary rounded-full mr-3 flex items-center justify-center">
|
||||
<div class="w-2 h-2 bg-thrill-primary rounded-full opacity-0 transition-opacity"></div>
|
||||
</div>
|
||||
<i class="fas fa-map-marked-alt mr-2 text-thrill-primary"></i>
|
||||
Parks
|
||||
</label>
|
||||
<label class="flex items-center p-3 border border-neutral-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-thrill-secondary/5 transition-colors">
|
||||
<input type="radio" name="search_type" value="rides" class="sr-only">
|
||||
<div class="w-4 h-4 border-2 border-thrill-secondary rounded-full mr-3 flex items-center justify-center">
|
||||
<div class="w-2 h-2 bg-thrill-secondary rounded-full opacity-0 transition-opacity"></div>
|
||||
</div>
|
||||
<i class="fas fa-rocket mr-2 text-thrill-secondary"></i>
|
||||
Rides
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Filters -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Location</label>
|
||||
<div class="space-y-3">
|
||||
<select name="country" class="form-select">
|
||||
<option value="">Any Country</option>
|
||||
<option value="US">United States</option>
|
||||
<option value="CA">Canada</option>
|
||||
<option value="GB">United Kingdom</option>
|
||||
<option value="DE">Germany</option>
|
||||
<option value="FR">France</option>
|
||||
<option value="JP">Japan</option>
|
||||
<option value="CN">China</option>
|
||||
</select>
|
||||
|
||||
<select name="region" class="form-select">
|
||||
<option value="">Any State/Region</option>
|
||||
</select>
|
||||
|
||||
<input type="text" name="city" placeholder="City" class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Park-Specific Filters -->
|
||||
<div id="park-filters" class="space-y-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Park Type</label>
|
||||
<select name="park_type" class="form-select">
|
||||
<option value="">Any Type</option>
|
||||
<option value="THEME_PARK">Theme Park</option>
|
||||
<option value="AMUSEMENT_PARK">Amusement Park</option>
|
||||
<option value="WATER_PARK">Water Park</option>
|
||||
<option value="FAMILY_ENTERTAINMENT">Family Entertainment</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Park Status</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="status" value="OPERATING" checked class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge-operating">Operating</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="status" value="CONSTRUCTION" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge-construction">Under Construction</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Minimum Rides</label>
|
||||
<input type="range" name="min_rides" min="0" max="100" value="0" class="w-full">
|
||||
<div class="flex justify-between text-sm text-neutral-500 mt-1">
|
||||
<span>0</span>
|
||||
<span id="min-rides-value">0</span>
|
||||
<span>100+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ride-Specific Filters -->
|
||||
<div id="ride-filters" class="space-y-6 hidden">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Thrill Level</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="thrill_level" value="MILD" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge bg-green-500/10 text-green-600 border-green-500/20">
|
||||
<i class="fas fa-leaf mr-1"></i>
|
||||
Family Friendly
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="thrill_level" value="MODERATE" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge bg-yellow-500/10 text-yellow-600 border-yellow-500/20">
|
||||
<i class="fas fa-star mr-1"></i>
|
||||
Moderate
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="thrill_level" value="HIGH" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge bg-orange-500/10 text-orange-600 border-orange-500/20">
|
||||
<i class="fas fa-bolt mr-1"></i>
|
||||
High Thrill
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="thrill_level" value="EXTREME" class="sr-only">
|
||||
<div class="checkbox-custom mr-3"></div>
|
||||
<span class="badge bg-red-500/10 text-red-600 border-red-500/20">
|
||||
<i class="fas fa-fire mr-1"></i>
|
||||
Extreme
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ride Category</label>
|
||||
<select name="category" class="form-select">
|
||||
<option value="">Any Category</option>
|
||||
<option value="ROLLER_COASTER">Roller Coaster</option>
|
||||
<option value="WATER_RIDE">Water Ride</option>
|
||||
<option value="DARK_RIDE">Dark Ride</option>
|
||||
<option value="FLAT_RIDE">Flat Ride</option>
|
||||
<option value="KIDDIE_RIDE">Kids Ride</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Minimum Height (ft)</label>
|
||||
<input type="range" name="min_height" min="0" max="500" value="0" class="w-full">
|
||||
<div class="flex justify-between text-sm text-neutral-500 mt-1">
|
||||
<span>0ft</span>
|
||||
<span id="min-height-value">0ft</span>
|
||||
<span>500ft+</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Minimum Speed (mph)</label>
|
||||
<input type="range" name="min_speed" min="0" max="150" value="0" class="w-full">
|
||||
<div class="flex justify-between text-sm text-neutral-500 mt-1">
|
||||
<span>0mph</span>
|
||||
<span id="min-speed-value">0mph</span>
|
||||
<span>150mph+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sort Options -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Sort By</label>
|
||||
<select name="sort" class="form-select">
|
||||
<option value="relevance">Relevance</option>
|
||||
<option value="name">Name (A-Z)</option>
|
||||
<option value="-name">Name (Z-A)</option>
|
||||
<option value="rating">Rating (Low to High)</option>
|
||||
<option value="-rating">Rating (High to Low)</option>
|
||||
<option value="opened_date">Oldest First</option>
|
||||
<option value="-opened_date">Newest First</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters -->
|
||||
<button type="button"
|
||||
id="clear-filters"
|
||||
class="btn-ghost w-full">
|
||||
<i class="fas fa-times mr-2"></i>
|
||||
Clear All Filters
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Results -->
|
||||
<div class="lg:col-span-3">
|
||||
<!-- Results Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">Search Results</h2>
|
||||
<p class="text-neutral-600 dark:text-neutral-400" id="results-count">
|
||||
Use filters to find your perfect adventure
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- View Toggle -->
|
||||
<div class="flex items-center space-x-2 bg-white dark:bg-neutral-800 rounded-lg p-1 border border-neutral-200 dark:border-neutral-700">
|
||||
<button class="p-2 rounded-md bg-thrill-primary text-white" id="grid-view">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</button>
|
||||
<button class="p-2 rounded-md text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-700" id="list-view">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Results Container -->
|
||||
<div id="search-results" class="min-h-96">
|
||||
<!-- Initial State -->
|
||||
<div class="text-center py-16">
|
||||
<div class="w-24 h-24 bg-gradient-to-r from-thrill-primary to-purple-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<i class="fas fa-search text-3xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-4">Ready to Explore?</h3>
|
||||
<p class="text-neutral-600 dark:text-neutral-400 max-w-md mx-auto">
|
||||
Use the filters on the left to discover amazing theme parks and thrilling rides that match your preferences.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load More Button -->
|
||||
<div id="load-more-container" class="text-center mt-8 hidden">
|
||||
<button class="btn-secondary btn-lg"
|
||||
hx-get="/search/results/"
|
||||
hx-target="#search-results"
|
||||
hx-swap="beforeend">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Load More Results
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced JavaScript for Advanced Search -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Search type toggle functionality
|
||||
const searchTypeRadios = document.querySelectorAll('input[name="search_type"]');
|
||||
const parkFilters = document.getElementById('park-filters');
|
||||
const rideFilters = document.getElementById('ride-filters');
|
||||
|
||||
searchTypeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
if (this.value === 'parks') {
|
||||
parkFilters.classList.remove('hidden');
|
||||
rideFilters.classList.add('hidden');
|
||||
} else {
|
||||
parkFilters.classList.add('hidden');
|
||||
rideFilters.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Update radio button visual state
|
||||
searchTypeRadios.forEach(r => {
|
||||
const indicator = r.parentElement.querySelector('div div');
|
||||
if (r.checked) {
|
||||
indicator.classList.remove('opacity-0');
|
||||
indicator.classList.add('opacity-100');
|
||||
} else {
|
||||
indicator.classList.remove('opacity-100');
|
||||
indicator.classList.add('opacity-0');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Range slider updates
|
||||
const rangeInputs = document.querySelectorAll('input[type="range"]');
|
||||
rangeInputs.forEach(input => {
|
||||
const updateValue = () => {
|
||||
const valueSpan = document.getElementById(input.name + '-value');
|
||||
if (valueSpan) {
|
||||
let value = input.value;
|
||||
if (input.name.includes('height')) value += 'ft';
|
||||
if (input.name.includes('speed')) value += 'mph';
|
||||
valueSpan.textContent = value;
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('input', updateValue);
|
||||
updateValue(); // Initial update
|
||||
});
|
||||
|
||||
// Checkbox styling
|
||||
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
const customCheckbox = checkbox.parentElement.querySelector('.checkbox-custom');
|
||||
if (customCheckbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
customCheckbox.classList.add('checked');
|
||||
} else {
|
||||
customCheckbox.classList.remove('checked');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clear filters functionality
|
||||
document.getElementById('clear-filters').addEventListener('click', function() {
|
||||
const form = document.getElementById('advanced-search-form');
|
||||
form.reset();
|
||||
|
||||
// Reset visual states
|
||||
searchTypeRadios[0].checked = true;
|
||||
searchTypeRadios[0].dispatchEvent(new Event('change'));
|
||||
|
||||
rangeInputs.forEach(input => {
|
||||
input.value = input.min;
|
||||
input.dispatchEvent(new Event('input'));
|
||||
});
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
const customCheckbox = checkbox.parentElement.querySelector('.checkbox-custom');
|
||||
if (customCheckbox) {
|
||||
customCheckbox.classList.remove('checked');
|
||||
}
|
||||
});
|
||||
|
||||
// Clear results
|
||||
document.getElementById('search-results').innerHTML = `
|
||||
<div class="text-center py-16">
|
||||
<div class="w-24 h-24 bg-gradient-to-r from-thrill-primary to-purple-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<i class="fas fa-search text-3xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-4">Ready to Explore?</h3>
|
||||
<p class="text-neutral-600 dark:text-neutral-400 max-w-md mx-auto">
|
||||
Use the filters on the left to discover amazing theme parks and thrilling rides that match your preferences.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// View toggle functionality
|
||||
const gridViewBtn = document.getElementById('grid-view');
|
||||
const listViewBtn = document.getElementById('list-view');
|
||||
|
||||
gridViewBtn.addEventListener('click', function() {
|
||||
this.classList.add('bg-thrill-primary', 'text-white');
|
||||
this.classList.remove('text-neutral-600', 'dark:text-neutral-400');
|
||||
listViewBtn.classList.remove('bg-thrill-primary', 'text-white');
|
||||
listViewBtn.classList.add('text-neutral-600', 'dark:text-neutral-400');
|
||||
|
||||
// Update results view
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
resultsContainer.classList.remove('list-view');
|
||||
resultsContainer.classList.add('grid-view');
|
||||
});
|
||||
|
||||
listViewBtn.addEventListener('click', function() {
|
||||
this.classList.add('bg-thrill-primary', 'text-white');
|
||||
this.classList.remove('text-neutral-600', 'dark:text-neutral-400');
|
||||
gridViewBtn.classList.remove('bg-thrill-primary', 'text-white');
|
||||
gridViewBtn.classList.add('text-neutral-600', 'dark:text-neutral-400');
|
||||
|
||||
// Update results view
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
resultsContainer.classList.remove('grid-view');
|
||||
resultsContainer.classList.add('list-view');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Custom CSS for checkboxes and enhanced styling -->
|
||||
<style>
|
||||
.checkbox-custom {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid #cbd5e1;
|
||||
border-radius: 0.25rem;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.checkbox-custom.checked {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.checkbox-custom.checked::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.grid-view .search-results-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.list-view .search-results-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.list-view .card-park,
|
||||
.list-view .card-ride {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.list-view .card-park-image,
|
||||
.list-view .card-ride-image {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.list-view .card-park-content,
|
||||
.list-view .card-ride-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user