mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:31:09 -05:00
- 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.
520 lines
18 KiB
HTML
520 lines
18 KiB
HTML
{% load static %}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta name="csrf-token" content="{{ csrf_token }}" />
|
|
|
|
<!-- SEO Meta Tags -->
|
|
<title>{% block title %}ThrillWiki{% endblock %}</title>
|
|
<meta name="description" content="{% block meta_description %}Your ultimate guide to theme parks and attractions worldwide. Discover thrilling rides, explore amazing parks, and share your adventures with fellow enthusiasts.{% endblock %}" />
|
|
<meta name="keywords" content="{% block meta_keywords %}theme parks, roller coasters, attractions, rides, amusement parks, Disney, Universal, Cedar Point{% endblock %}" />
|
|
<meta name="author" content="ThrillWiki" />
|
|
<meta name="robots" content="{% block meta_robots %}index, follow{% endblock %}" />
|
|
<link rel="canonical" href="{% block canonical_url %}{{ request.scheme }}://{{ request.get_host }}{{ request.path }}{% endblock %}" />
|
|
|
|
<!-- Open Graph / Facebook -->
|
|
<meta property="og:type" content="{% block og_type %}website{% endblock %}" />
|
|
<meta property="og:url" content="{% block og_url %}{{ request.build_absolute_uri|default:'' }}{% endblock %}" />
|
|
<meta property="og:title" content="{% block og_title %}ThrillWiki{% endblock %}" />
|
|
<meta property="og:description" content="{% block og_description %}Your ultimate guide to theme parks and attractions worldwide. Discover thrilling rides, explore amazing parks, and share your adventures with fellow enthusiasts.{% endblock %}" />
|
|
<meta property="og:image" content="{% block og_image %}{% load static %}{{ request.scheme }}://{{ request.get_host }}{% static 'images/placeholders/default-park.jpg' %}{% endblock %}" />
|
|
<meta property="og:image:width" content="1200" />
|
|
<meta property="og:image:height" content="630" />
|
|
<meta property="og:site_name" content="ThrillWiki" />
|
|
<meta property="og:locale" content="en_US" />
|
|
|
|
<!-- Twitter -->
|
|
<meta name="twitter:card" content="{% block twitter_card %}summary_large_image{% endblock %}" />
|
|
<meta name="twitter:url" content="{% block twitter_url %}{{ request.build_absolute_uri|default:'' }}{% endblock %}" />
|
|
<meta name="twitter:title" content="{% block twitter_title %}ThrillWiki{% endblock %}" />
|
|
<meta name="twitter:description" content="{% block twitter_description %}Your ultimate guide to theme parks and attractions worldwide. Discover thrilling rides, explore amazing parks, and share your adventures with fellow enthusiasts.{% endblock %}" />
|
|
<meta name="twitter:image" content="{% block twitter_image %}{% load static %}{{ request.scheme }}://{{ request.get_host }}{% static 'images/placeholders/default-park.jpg' %}{% endblock %}" />
|
|
<meta name="twitter:creator" content="@ThrillWiki" />
|
|
<meta name="twitter:site" content="@ThrillWiki" />
|
|
|
|
<!-- Resource Hints for Performance -->
|
|
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
|
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
|
|
<link rel="dns-prefetch" href="//unpkg.com" />
|
|
<link rel="dns-prefetch" href="//cdnjs.cloudflare.com" />
|
|
{% block extra_dns_prefetch %}{% endblock %}
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
{% block extra_preconnect %}{% endblock %}
|
|
|
|
<!-- Preload Critical Resources -->
|
|
{% block critical_resources %}
|
|
<link rel="preload" href="{% static 'css/tailwind.css' %}" as="style" />
|
|
<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 %}
|
|
|
|
<!-- Module Preload for Modern Browsers -->
|
|
{% block module_preload %}{% endblock %}
|
|
|
|
<!-- Google Fonts with performance optimizations -->
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
|
|
<!-- HTMX -->
|
|
<script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"></script>
|
|
|
|
<!-- Alpine.js (must load after components) -->
|
|
<script defer src="{% static 'js/alpine.min.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" />
|
|
|
|
<!-- Font Awesome -->
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
|
|
integrity="sha512-9usAa10IRO0HhonpyAIVpjrylPvoDwiPUiKdWk5t3PyolY1cOd4DSE0Ga+ri4AuTroPR5aQvXU9xC6qOPnzFeg=="
|
|
crossorigin="anonymous"
|
|
/>
|
|
|
|
|
|
<!-- Structured Data (JSON-LD) -->
|
|
{% 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"
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body
|
|
class="flex flex-col min-h-screen text-gray-900 bg-gradient-to-br from-white via-blue-50 to-indigo-50 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950 dark:text-white"
|
|
{% block body_attributes %}{% endblock %}
|
|
>
|
|
<!-- Skip to content link for accessibility -->
|
|
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">Skip to main content</a>
|
|
<!-- Enhanced Header -->
|
|
{% include 'components/layout/enhanced_header.html' %}
|
|
|
|
<!-- Flash Messages -->
|
|
{% if messages %}
|
|
<div class="fixed top-0 right-0 z-50 p-4 space-y-4" role="alert" aria-live="polite" aria-label="Notifications">
|
|
{% for message in messages %}
|
|
<div
|
|
class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}"
|
|
role="alert"
|
|
aria-describedby="alert-{{ forloop.counter }}"
|
|
>
|
|
<span id="alert-{{ forloop.counter }}">{{ message }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Main Content -->
|
|
<main id="main-content" class="container flex-grow px-6 py-8 mx-auto" role="main">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer
|
|
class="mt-auto border-t bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg border-gray-200/50 dark:border-gray-700/50"
|
|
role="contentinfo"
|
|
aria-label="Site footer"
|
|
>
|
|
<div class="container px-6 py-6 mx-auto">
|
|
<div class="flex items-center justify-between">
|
|
<div class="text-gray-600 dark:text-gray-400">
|
|
<p>© {% now "Y" %} ThrillWiki. All rights reserved.</p>
|
|
</div>
|
|
<nav class="space-x-4" role="navigation" aria-label="Footer links">
|
|
<a
|
|
href="{% url 'terms' %}"
|
|
class="text-gray-600 transition-colors hover:text-primary dark:text-gray-400 dark:hover:text-primary"
|
|
>Terms</a
|
|
>
|
|
<a
|
|
href="{% url 'privacy' %}"
|
|
class="text-gray-600 transition-colors hover:text-primary dark:text-gray-400 dark:hover:text-primary"
|
|
>Privacy</a
|
|
>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- Global Auth Modal -->
|
|
<c-auth_modal />
|
|
|
|
<!-- Global Toast Container -->
|
|
<c-toast_container />
|
|
|
|
<!-- 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>
|
|
</html>
|