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:
pacnpal
2025-09-24 23:10:48 -04:00
parent 4373d18176
commit b1c369c1bb
39 changed files with 5202 additions and 824 deletions

View File

@@ -0,0 +1,828 @@
/* ThrillWiki Design System CSS */
/* Last Updated: 2025-01-15 */
/* ===== CSS CUSTOM PROPERTIES ===== */
:root {
/* Thrill Colors - Excitement & Adventure */
--thrill-primary: #6366f1;
--thrill-primary-dark: #4f46e5;
--thrill-primary-light: #818cf8;
/* Adventure Colors - Energy & Fun */
--thrill-secondary: #f59e0b;
--thrill-secondary-dark: #d97706;
--thrill-secondary-light: #fbbf24;
/* Status Colors - Clear Communication */
--thrill-success: #10b981;
--thrill-warning: #f59e0b;
--thrill-danger: #ef4444;
--thrill-info: #3b82f6;
/* Neutral Palette - Light Mode */
--neutral-50: #f8fafc;
--neutral-100: #f1f5f9;
--neutral-200: #e2e8f0;
--neutral-300: #cbd5e1;
--neutral-400: #94a3b8;
--neutral-500: #64748b;
--neutral-600: #475569;
--neutral-700: #334155;
--neutral-800: #1e293b;
--neutral-900: #0f172a;
/* Gradients */
--gradient-hero: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%);
--gradient-hero-dark: linear-gradient(135deg, #4338ca 0%, #7c3aed 50%, #db2777 100%);
--gradient-bg-light: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 50%, #ede9fe 100%);
--gradient-bg-dark: linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #581c87 100%);
--gradient-card: linear-gradient(145deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
--gradient-card-hover: linear-gradient(145deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.08) 100%);
/* Typography */
--font-primary: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* Spacing */
--space-0: 0;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--space-16: 4rem;
--space-20: 5rem;
--space-24: 6rem;
--space-32: 8rem;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
/* Border Radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
--radius-full: 9999px;
}
/* Dark mode color adjustments */
.dark {
--neutral-50: #0f172a;
--neutral-100: #1e293b;
--neutral-200: #334155;
--neutral-300: #475569;
--neutral-400: #64748b;
--neutral-500: #94a3b8;
--neutral-600: #cbd5e1;
--neutral-700: #e2e8f0;
--neutral-800: #f1f5f9;
--neutral-900: #f8fafc;
}
/* ===== BASE STYLES ===== */
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ===== BUTTON COMPONENTS ===== */
.btn {
@apply inline-flex items-center justify-center font-semibold transition-all duration-200 ease-out;
@apply focus:outline-none focus:ring-4 disabled:opacity-50 disabled:cursor-not-allowed;
@apply select-none whitespace-nowrap;
}
.btn-primary {
@apply btn px-6 py-3 text-white rounded-xl shadow-lg;
background: var(--gradient-hero);
@apply hover:shadow-xl hover:scale-105 active:scale-95;
@apply focus:ring-4;
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.3);
}
.btn-primary:focus {
box-shadow: var(--shadow-lg), 0 0 0 4px rgba(99, 102, 241, 0.3);
}
.btn-secondary {
@apply btn px-6 py-3 bg-white dark:bg-neutral-800 rounded-xl shadow-md;
@apply text-thrill-primary dark:text-thrill-primary-light;
@apply border border-thrill-primary/20 hover:bg-thrill-primary/5;
@apply hover:shadow-lg hover:scale-105 active:scale-95;
@apply focus:ring-thrill-primary/30;
}
.btn-ghost {
@apply btn px-4 py-2 text-neutral-600 dark:text-neutral-400;
@apply font-medium rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800;
@apply hover:text-neutral-900 dark:hover:text-neutral-100;
@apply focus:ring-neutral-300 dark:focus:ring-neutral-600;
}
.btn-sm {
@apply px-3 py-1.5 text-sm;
}
.btn-lg {
@apply px-8 py-4 text-lg;
}
/* ===== CARD COMPONENTS ===== */
.card {
@apply bg-white/80 dark:bg-neutral-800/80 backdrop-blur-lg;
@apply border border-neutral-200/50 dark:border-neutral-700/50;
@apply rounded-2xl shadow-lg transition-all duration-300 ease-out;
}
.card:hover {
@apply shadow-xl;
transform: translateY(-4px) scale(1.02);
}
.card-feature {
@apply card p-8 relative overflow-hidden;
}
.card-feature::before {
content: '';
@apply absolute inset-0 transition-all duration-300;
background: var(--gradient-card);
}
.card-feature:hover::before {
background: var(--gradient-card-hover);
}
.card-park {
@apply card group cursor-pointer;
@apply hover:ring-2 hover:ring-thrill-primary/30;
}
.card-park-image {
@apply aspect-video w-full object-cover rounded-t-2xl;
@apply group-hover:scale-105 transition-transform duration-500 ease-out;
@apply overflow-hidden;
}
.card-park-content {
@apply p-6 space-y-4 relative z-10;
}
.card-ride {
@apply card group cursor-pointer;
@apply hover:ring-2 hover:ring-thrill-secondary/30;
}
.card-ride-image {
@apply aspect-square w-full object-cover rounded-t-2xl;
@apply group-hover:scale-110 transition-transform duration-700 ease-out;
@apply overflow-hidden;
}
.card-ride-content {
@apply p-4 space-y-3 relative z-10;
}
/* ===== FORM COMPONENTS ===== */
.form-group {
@apply space-y-2;
}
.form-label {
@apply block text-sm font-semibold text-neutral-700 dark:text-neutral-300;
}
.form-input {
@apply w-full px-4 py-3 bg-white dark:bg-neutral-800;
@apply border border-neutral-300 dark:border-neutral-600;
@apply rounded-xl shadow-sm focus:shadow-md;
@apply text-neutral-900 dark:text-neutral-100;
@apply placeholder-neutral-500 dark:placeholder-neutral-400;
@apply focus:outline-none focus:ring-2 focus:ring-thrill-primary/50;
@apply focus:border-thrill-primary dark:focus:border-thrill-primary-light;
@apply transition-all duration-200 ease-out;
}
.form-input:focus {
transform: translateY(-1px);
}
.form-textarea {
@apply form-input resize-none;
min-height: 120px;
}
.form-select {
@apply form-input cursor-pointer;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.75rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.form-error {
@apply text-sm text-thrill-danger mt-1;
}
.form-input-error {
@apply border-thrill-danger focus:ring-thrill-danger/50;
@apply focus:border-thrill-danger;
}
/* ===== STATUS BADGES ===== */
.badge {
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold;
@apply border transition-all duration-150;
}
.badge-operating {
@apply badge bg-thrill-success/10 text-thrill-success border-thrill-success/20;
}
.badge-construction {
@apply badge bg-thrill-warning/10 text-thrill-warning border-thrill-warning/20;
}
.badge-closed {
@apply badge bg-thrill-danger/10 text-thrill-danger border-thrill-danger/20;
}
.badge-info {
@apply badge bg-thrill-info/10 text-thrill-info border-thrill-info/20;
}
.badge-lg {
@apply px-4 py-2 text-sm;
}
/* ===== NAVIGATION COMPONENTS ===== */
.nav-link {
@apply px-4 py-2 text-neutral-600 dark:text-neutral-400;
@apply font-medium rounded-lg transition-all duration-150;
@apply hover:bg-neutral-100 dark:hover:bg-neutral-800;
@apply hover:text-neutral-900 dark:hover:text-neutral-100;
@apply focus:outline-none focus:ring-2 focus:ring-neutral-300 dark:focus:ring-neutral-600;
}
.nav-link-active {
@apply nav-link bg-thrill-primary/10 text-thrill-primary;
@apply hover:bg-thrill-primary/15;
}
.nav-brand {
@apply text-2xl font-bold;
background: var(--gradient-hero);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ===== HERO SECTIONS ===== */
.hero {
@apply relative overflow-hidden;
background: var(--gradient-bg-light);
}
.dark .hero {
background: var(--gradient-bg-dark);
}
.hero-content {
@apply relative z-10 text-center space-y-8;
}
.hero-title {
@apply text-4xl md:text-6xl lg:text-7xl font-bold;
background: var(--gradient-hero);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.1;
}
.hero-subtitle {
@apply text-xl md:text-2xl text-neutral-600 dark:text-neutral-400;
@apply max-w-3xl mx-auto;
}
.hero-cta {
@apply flex flex-col sm:flex-row gap-4 justify-center items-center;
}
/* ===== ANIMATION CLASSES ===== */
.hover-lift {
@apply transition-all duration-300 ease-out;
}
.hover-lift:hover {
transform: translateY(-4px) scale(1.02);
@apply shadow-xl;
}
.pulse-glow {
animation: pulse-glow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4);
}
50% {
box-shadow: 0 0 0 10px rgba(99, 102, 241, 0);
}
}
.slide-in-up {
animation: slideInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.slide-in-right {
animation: slideInRight 0.5s ease-out forwards;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* ===== HTMX TRANSITIONS ===== */
.htmx-transition {
view-transition-name: main-content;
}
::view-transition-old(main-content) {
animation: 300ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(main-content) {
animation: 400ms cubic-bezier(0, 0, 0.2, 1) 100ms both fade-in,
600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes slide-from-right {
from { transform: translateX(30px); }
}
@keyframes slide-to-left {
to { transform: translateX(-30px); }
}
/* HTMX Loading States */
.htmx-request {
@apply opacity-75 pointer-events-none;
}
.htmx-request .loading-spinner {
@apply opacity-100;
}
/* ===== LOADING STATES ===== */
.loading-skeleton {
@apply bg-gradient-to-r from-neutral-200 via-neutral-100 to-neutral-200;
@apply dark:from-neutral-700 dark:via-neutral-600 dark:to-neutral-700;
@apply animate-pulse rounded;
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.loading-spinner {
@apply opacity-0 transition-opacity duration-200;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* ===== GRID SYSTEMS ===== */
.grid-auto-fit-xs {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.grid-auto-fit-sm {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.grid-auto-fit-md {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
}
.grid-auto-fit-lg {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 2rem;
}
/* ===== ACCESSIBILITY ===== */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
padding: 0.25rem;
margin: 0;
overflow: visible;
clip: auto;
white-space: normal;
}
.focus-visible {
@apply focus:outline-none focus-visible:ring-2 focus-visible:ring-thrill-primary;
@apply focus-visible:ring-offset-2 focus-visible:ring-offset-white;
@apply dark:focus-visible:ring-offset-neutral-900;
}
/* ===== RESPONSIVE UTILITIES ===== */
.container-xs { max-width: 480px; margin: 0 auto; }
.container-sm { max-width: 640px; margin: 0 auto; }
.container-md { max-width: 768px; margin: 0 auto; }
.container-lg { max-width: 1024px; margin: 0 auto; }
.container-xl { max-width: 1280px; margin: 0 auto; }
.container-2xl { max-width: 1536px; margin: 0 auto; }
/* ===== MOTION PREFERENCES ===== */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.hover-lift:hover {
transform: none;
}
}
/* ===== PRINT STYLES ===== */
@media print {
.no-print {
display: none !important;
}
.card {
@apply shadow-none border border-neutral-300;
break-inside: avoid;
}
.btn {
@apply shadow-none;
}
}
/* ===== HIGH CONTRAST MODE ===== */
@media (prefers-contrast: high) {
.card {
@apply border-2 border-neutral-900 dark:border-neutral-100;
}
.btn-primary {
@apply bg-neutral-900 dark:bg-neutral-100;
@apply text-neutral-100 dark:text-neutral-900;
}
.form-input {
@apply border-2 border-neutral-900 dark:border-neutral-100;
}
}
/* ===== SCROLL TO TOP BUTTON ===== */
.scroll-to-top {
@apply fixed bottom-8 right-8 w-12 h-12 bg-thrill-primary text-white;
@apply rounded-full shadow-lg hover:shadow-xl transition-all duration-300;
@apply opacity-0 pointer-events-none z-40 flex items-center justify-center;
@apply hover:scale-110 active:scale-95;
}
.scroll-to-top.visible {
@apply opacity-100 pointer-events-auto;
}
.scroll-to-top:hover {
background: var(--gradient-hero);
}
/* ===== REVEAL ANIMATIONS ===== */
.reveal-element {
@apply opacity-0 translate-y-8 transition-all duration-700 ease-out;
}
.reveal-element.revealed {
@apply opacity-100 translate-y-0;
}
.reveal-element.delay-100 { transition-delay: 100ms; }
.reveal-element.delay-200 { transition-delay: 200ms; }
.reveal-element.delay-300 { transition-delay: 300ms; }
.reveal-element.delay-500 { transition-delay: 500ms; }
/* ===== PARALLAX EFFECTS ===== */
.parallax-element {
@apply transition-transform duration-75 ease-out;
}
/* ===== ENHANCED CARD ANIMATIONS ===== */
.card-enhanced {
@apply transition-all duration-300 ease-out;
@apply hover:shadow-2xl hover:-translate-y-2;
}
.card-enhanced .card-image {
@apply transition-transform duration-500 ease-out overflow-hidden;
}
.card-enhanced:hover .card-image {
@apply scale-105;
}
.card-enhanced .card-overlay {
@apply absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent;
@apply opacity-0 transition-opacity duration-300;
}
.card-enhanced:hover .card-overlay {
@apply opacity-100;
}
.card-enhanced .card-hidden-content {
@apply opacity-0 translate-y-4 transition-all duration-300;
}
.card-enhanced:hover .card-hidden-content {
@apply opacity-100 translate-y-0;
}
/* ===== FAVORITE BUTTON ANIMATIONS ===== */
.favorite-btn {
@apply transition-all duration-200 ease-out;
@apply hover:scale-110 active:scale-95;
}
.favorite-btn.favorited {
@apply text-red-500;
animation: heartbeat 0.6s ease-in-out;
}
@keyframes heartbeat {
0% { transform: scale(1); }
25% { transform: scale(1.2); }
50% { transform: scale(1); }
75% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* ===== SEARCH RESULT ANIMATIONS ===== */
.search-results-dropdown {
@apply bg-white dark:bg-neutral-800 rounded-xl shadow-xl border;
@apply border-neutral-200 dark:border-neutral-700 max-h-96 overflow-y-auto;
animation: slideInDown 0.3s ease-out;
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.search-result-item {
@apply flex items-center gap-3 p-3 hover:bg-neutral-50 dark:hover:bg-neutral-700;
@apply cursor-pointer transition-colors duration-150;
@apply border-b border-neutral-100 dark:border-neutral-700 last:border-b-0;
}
.search-result-item:hover {
@apply bg-thrill-primary/5 dark:bg-thrill-primary/10;
}
.search-group-title {
@apply text-xs font-semibold text-neutral-500 dark:text-neutral-400;
@apply uppercase tracking-wider px-3 py-2 bg-neutral-50 dark:bg-neutral-800;
}
.search-loading {
@apply flex items-center justify-center gap-2 p-4 text-neutral-500;
}
.search-no-results {
@apply flex flex-col items-center justify-center gap-2 p-6 text-neutral-500;
}
.search-error {
@apply flex items-center justify-center gap-2 p-4 text-red-500;
}
/* ===== NOTIFICATION/TOAST STYLES ===== */
.toast-container {
@apply fixed top-4 right-4 z-50 space-y-4 pointer-events-none;
}
.toast {
@apply bg-white dark:bg-neutral-800 rounded-xl shadow-xl border;
@apply border-neutral-200 dark:border-neutral-700 p-4 min-w-80;
@apply pointer-events-auto transform transition-all duration-300;
@apply translate-x-full opacity-0;
}
.toast.visible {
@apply translate-x-0 opacity-100;
}
.toast.hide {
@apply translate-x-full opacity-0;
}
.toast-success {
@apply border-l-4 border-l-thrill-success;
}
.toast-error {
@apply border-l-4 border-l-thrill-danger;
}
.toast-warning {
@apply border-l-4 border-l-thrill-warning;
}
.toast-info {
@apply border-l-4 border-l-thrill-info;
}
.toast-progress {
@apply absolute bottom-0 left-0 h-1 bg-current opacity-20 transition-all duration-100;
}
/* ===== FORM ENHANCEMENTS ===== */
.form-floating-label {
@apply relative;
}
.form-floating-label input {
@apply pt-6 pb-2;
}
.form-floating-label label {
@apply absolute left-4 top-4 text-neutral-500 transition-all duration-200;
@apply pointer-events-none;
}
.form-floating-label input:focus + label,
.form-floating-label input:not(:placeholder-shown) + label {
@apply text-xs top-2 text-thrill-primary;
}
.form-validation-success {
@apply border-thrill-success focus:ring-thrill-success/50;
}
.form-validation-error {
@apply border-thrill-danger focus:ring-thrill-danger/50;
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
/* ===== MODAL ENHANCEMENTS ===== */
.modal-backdrop {
@apply fixed inset-0 bg-black/50 backdrop-blur-sm z-40;
@apply transition-opacity duration-300;
}
.modal-content {
@apply bg-white dark:bg-neutral-800 rounded-2xl shadow-2xl;
@apply transform transition-all duration-300;
@apply scale-95 opacity-0;
}
.modal-content.show {
@apply scale-100 opacity-100;
}
/* ===== DROPDOWN ENHANCEMENTS ===== */
.dropdown-menu {
@apply bg-white dark:bg-neutral-800 rounded-xl shadow-xl border;
@apply border-neutral-200 dark:border-neutral-700 py-2;
@apply transform transition-all duration-200 origin-top;
@apply scale-95 opacity-0 pointer-events-none;
}
.dropdown-menu.show {
@apply scale-100 opacity-100 pointer-events-auto;
}
.dropdown-item {
@apply block w-full px-4 py-2 text-left text-neutral-700 dark:text-neutral-300;
@apply hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors duration-150;
}
/* ===== CUSTOM SCROLLBAR ===== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-neutral-100 dark:bg-neutral-800;
}
::-webkit-scrollbar-thumb {
@apply bg-neutral-400 dark:bg-neutral-600 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-neutral-500 dark:bg-neutral-500;
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: var(--neutral-400) var(--neutral-100);
}
.dark * {
scrollbar-color: var(--neutral-600) var(--neutral-800);
}

View File

@@ -0,0 +1,799 @@
/**
* ThrillWiki Enhanced JavaScript
* Advanced interactions, animations, and UI enhancements
* Last Updated: 2025-01-15
*/
// Global ThrillWiki namespace
window.ThrillWiki = window.ThrillWiki || {};
(function(TW) {
'use strict';
// Configuration
TW.config = {
animationDuration: 300,
scrollOffset: 80,
debounceDelay: 300,
apiEndpoints: {
search: '/api/search/',
favorites: '/api/favorites/',
notifications: '/api/notifications/'
}
};
// Utility functions
TW.utils = {
// Debounce function for performance
debounce: function(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// Throttle function for scroll events
throttle: function(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
},
// Smooth scroll to element
scrollTo: function(element, offset = TW.config.scrollOffset) {
const targetPosition = element.offsetTop - offset;
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
},
// Check if element is in viewport
isInViewport: function(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
},
// Format numbers with commas
formatNumber: function(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
// Generate unique ID
generateId: function() {
return 'tw-' + Math.random().toString(36).substr(2, 9);
}
};
// Animation system
TW.animations = {
// Fade in animation
fadeIn: function(element, duration = TW.config.animationDuration) {
element.style.opacity = '0';
element.style.display = 'block';
const fadeEffect = setInterval(() => {
if (!element.style.opacity) {
element.style.opacity = 0;
}
if (element.style.opacity < 1) {
element.style.opacity = parseFloat(element.style.opacity) + 0.1;
} else {
clearInterval(fadeEffect);
}
}, duration / 10);
},
// Slide in from bottom
slideInUp: function(element, duration = TW.config.animationDuration) {
element.style.transform = 'translateY(30px)';
element.style.opacity = '0';
element.style.transition = `all ${duration}ms cubic-bezier(0.16, 1, 0.3, 1)`;
setTimeout(() => {
element.style.transform = 'translateY(0)';
element.style.opacity = '1';
}, 10);
},
// Pulse effect
pulse: function(element, intensity = 1.05) {
element.style.transition = 'transform 0.15s ease-out';
element.style.transform = `scale(${intensity})`;
setTimeout(() => {
element.style.transform = 'scale(1)';
}, 150);
},
// Shake effect for errors
shake: function(element) {
element.style.animation = 'shake 0.5s ease-in-out';
setTimeout(() => {
element.style.animation = '';
}, 500);
}
};
// Enhanced search functionality
TW.search = {
init: function() {
this.setupQuickSearch();
this.setupAdvancedSearch();
this.setupSearchSuggestions();
},
setupQuickSearch: function() {
const quickSearchInputs = document.querySelectorAll('[data-quick-search]');
quickSearchInputs.forEach(input => {
const debouncedSearch = TW.utils.debounce(this.performQuickSearch.bind(this), TW.config.debounceDelay);
input.addEventListener('input', (e) => {
const query = e.target.value.trim();
if (query.length >= 2) {
debouncedSearch(query, e.target);
} else {
this.clearSearchResults(e.target);
}
});
// Handle keyboard navigation
input.addEventListener('keydown', this.handleSearchKeyboard.bind(this));
});
},
performQuickSearch: function(query, inputElement) {
const resultsContainer = document.getElementById(inputElement.dataset.quickSearch);
if (!resultsContainer) return;
// Show loading state
resultsContainer.innerHTML = this.getLoadingHTML();
resultsContainer.classList.remove('hidden');
// Perform search
fetch(`${TW.config.apiEndpoints.search}?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
this.displaySearchResults(data, resultsContainer);
})
.catch(error => {
console.error('Search error:', error);
resultsContainer.innerHTML = this.getErrorHTML();
});
},
displaySearchResults: function(data, container) {
if (!data.results || data.results.length === 0) {
container.innerHTML = this.getNoResultsHTML();
return;
}
let html = '<div class="search-results-dropdown">';
// Group results by type
const groupedResults = this.groupResultsByType(data.results);
Object.keys(groupedResults).forEach(type => {
if (groupedResults[type].length > 0) {
html += `<div class="search-group">
<h4 class="search-group-title">${this.getTypeTitle(type)}</h4>
<div class="search-group-items">`;
groupedResults[type].forEach(result => {
html += this.getResultItemHTML(result);
});
html += '</div></div>';
}
});
html += '</div>';
container.innerHTML = html;
// Add click handlers
this.attachResultClickHandlers(container);
},
getResultItemHTML: function(result) {
return `
<div class="search-result-item" data-url="${result.url}" data-type="${result.type}">
<div class="search-result-icon">
<i class="fas fa-${this.getTypeIcon(result.type)}"></i>
</div>
<div class="search-result-content">
<div class="search-result-title">${result.name}</div>
<div class="search-result-subtitle">${result.subtitle || ''}</div>
</div>
${result.image ? `<div class="search-result-image">
<img src="${result.image}" alt="${result.name}" loading="lazy">
</div>` : ''}
</div>
`;
},
groupResultsByType: function(results) {
return results.reduce((groups, result) => {
const type = result.type || 'other';
if (!groups[type]) groups[type] = [];
groups[type].push(result);
return groups;
}, {});
},
getTypeTitle: function(type) {
const titles = {
'park': 'Theme Parks',
'ride': 'Rides & Attractions',
'location': 'Locations',
'other': 'Other Results'
};
return titles[type] || 'Results';
},
getTypeIcon: function(type) {
const icons = {
'park': 'map-marked-alt',
'ride': 'rocket',
'location': 'map-marker-alt',
'other': 'search'
};
return icons[type] || 'search';
},
getLoadingHTML: function() {
return `
<div class="search-loading">
<div class="loading-spinner opacity-100">
<i class="fas fa-spinner text-thrill-primary"></i>
</div>
<span>Searching...</span>
</div>
`;
},
getNoResultsHTML: function() {
return `
<div class="search-no-results">
<i class="fas fa-search text-neutral-400"></i>
<span>No results found</span>
</div>
`;
},
getErrorHTML: function() {
return `
<div class="search-error">
<i class="fas fa-exclamation-triangle text-red-500"></i>
<span>Search error. Please try again.</span>
</div>
`;
},
attachResultClickHandlers: function(container) {
const resultItems = container.querySelectorAll('.search-result-item');
resultItems.forEach(item => {
item.addEventListener('click', (e) => {
const url = item.dataset.url;
if (url) {
// Use HTMX if available, otherwise navigate normally
if (window.htmx) {
htmx.ajax('GET', url, {
target: '#main-content',
swap: 'innerHTML transition:true'
});
} else {
window.location.href = url;
}
// Clear search
this.clearSearchResults(container.previousElementSibling);
}
});
});
},
clearSearchResults: function(inputElement) {
const resultsContainer = document.getElementById(inputElement.dataset.quickSearch);
if (resultsContainer) {
resultsContainer.classList.add('hidden');
resultsContainer.innerHTML = '';
}
},
handleSearchKeyboard: function(e) {
// Handle escape key to close results
if (e.key === 'Escape') {
this.clearSearchResults(e.target);
}
}
};
// Enhanced card interactions
TW.cards = {
init: function() {
this.setupCardHovers();
this.setupFavoriteButtons();
this.setupCardAnimations();
},
setupCardHovers: function() {
const cards = document.querySelectorAll('.card-park, .card-ride, .card-feature');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
this.onCardHover(card);
});
card.addEventListener('mouseleave', () => {
this.onCardLeave(card);
});
});
},
onCardHover: function(card) {
// Add subtle glow effect
card.style.boxShadow = '0 20px 40px rgba(99, 102, 241, 0.15)';
// Animate card image if present
const image = card.querySelector('.card-park-image, .card-ride-image');
if (image) {
image.style.transform = 'scale(1.05)';
}
// Show hidden elements
const hiddenElements = card.querySelectorAll('.opacity-0');
hiddenElements.forEach(el => {
el.style.opacity = '1';
el.style.transform = 'translateY(0)';
});
},
onCardLeave: function(card) {
// Reset styles
card.style.boxShadow = '';
const image = card.querySelector('.card-park-image, .card-ride-image');
if (image) {
image.style.transform = '';
}
},
setupFavoriteButtons: function() {
document.addEventListener('click', (e) => {
if (e.target.closest('[data-favorite-toggle]')) {
e.preventDefault();
e.stopPropagation();
const button = e.target.closest('[data-favorite-toggle]');
this.toggleFavorite(button);
}
});
},
toggleFavorite: function(button) {
const itemId = button.dataset.favoriteToggle;
const itemType = button.dataset.favoriteType || 'park';
// Optimistic UI update
const icon = button.querySelector('i');
const isFavorited = icon.classList.contains('fas');
if (isFavorited) {
icon.classList.remove('fas', 'text-red-500');
icon.classList.add('far', 'text-neutral-600', 'dark:text-neutral-400');
} else {
icon.classList.remove('far', 'text-neutral-600', 'dark:text-neutral-400');
icon.classList.add('fas', 'text-red-500');
}
// Animate button
TW.animations.pulse(button, 1.2);
// Send request to server
fetch(`${TW.config.apiEndpoints.favorites}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken()
},
body: JSON.stringify({
item_id: itemId,
item_type: itemType,
action: isFavorited ? 'remove' : 'add'
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
// Revert optimistic update
if (isFavorited) {
icon.classList.remove('far', 'text-neutral-600', 'dark:text-neutral-400');
icon.classList.add('fas', 'text-red-500');
} else {
icon.classList.remove('fas', 'text-red-500');
icon.classList.add('far', 'text-neutral-600', 'dark:text-neutral-400');
}
TW.notifications.show('Error updating favorite', 'error');
}
})
.catch(error => {
console.error('Favorite toggle error:', error);
TW.notifications.show('Error updating favorite', 'error');
});
},
getCSRFToken: function() {
const token = document.querySelector('[name=csrfmiddlewaretoken]');
return token ? token.value : '';
}
};
// Enhanced notifications system
TW.notifications = {
container: null,
init: function() {
this.createContainer();
this.setupAutoHide();
},
createContainer: function() {
if (!this.container) {
this.container = document.createElement('div');
this.container.id = 'tw-notifications';
this.container.className = 'fixed top-4 right-4 z-50 space-y-4';
document.body.appendChild(this.container);
}
},
show: function(message, type = 'info', duration = 5000) {
const notification = this.createNotification(message, type);
this.container.appendChild(notification);
// Animate in
setTimeout(() => {
notification.classList.add('show');
}, 10);
// Auto hide
if (duration > 0) {
setTimeout(() => {
this.hide(notification);
}, duration);
}
return notification;
},
createNotification: function(message, type) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
const typeIcons = {
'success': 'check-circle',
'error': 'exclamation-circle',
'warning': 'exclamation-triangle',
'info': 'info-circle'
};
notification.innerHTML = `
<div class="notification-content">
<i class="fas fa-${typeIcons[type] || 'info-circle'} notification-icon"></i>
<span class="notification-message">${message}</span>
<button class="notification-close" onclick="ThrillWiki.notifications.hide(this.closest('.notification'))">
<i class="fas fa-times"></i>
</button>
</div>
`;
return notification;
},
hide: function(notification) {
notification.classList.add('hide');
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
},
setupAutoHide: function() {
// Auto-hide notifications on page navigation
if (window.htmx) {
document.addEventListener('htmx:beforeRequest', () => {
const notifications = this.container.querySelectorAll('.notification');
notifications.forEach(notification => {
this.hide(notification);
});
});
}
}
};
// Enhanced scroll effects
TW.scroll = {
init: function() {
this.setupParallax();
this.setupRevealAnimations();
this.setupScrollToTop();
},
setupParallax: function() {
const parallaxElements = document.querySelectorAll('[data-parallax]');
if (parallaxElements.length > 0) {
const handleScroll = TW.utils.throttle(() => {
const scrolled = window.pageYOffset;
parallaxElements.forEach(element => {
const speed = parseFloat(element.dataset.parallax) || 0.5;
const yPos = -(scrolled * speed);
element.style.transform = `translateY(${yPos}px)`;
});
}, 16); // ~60fps
window.addEventListener('scroll', handleScroll);
}
},
setupRevealAnimations: function() {
const revealElements = document.querySelectorAll('[data-reveal]');
if (revealElements.length > 0) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
const animationType = element.dataset.reveal || 'fadeIn';
const delay = parseInt(element.dataset.revealDelay) || 0;
setTimeout(() => {
element.classList.add('revealed');
if (TW.animations[animationType]) {
TW.animations[animationType](element);
}
}, delay);
observer.unobserve(element);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
revealElements.forEach(element => {
observer.observe(element);
});
}
},
setupScrollToTop: function() {
const scrollToTopBtn = document.createElement('button');
scrollToTopBtn.id = 'scroll-to-top';
scrollToTopBtn.className = 'fixed bottom-8 right-8 w-12 h-12 bg-thrill-primary text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300 opacity-0 pointer-events-none z-40';
scrollToTopBtn.innerHTML = '<i class="fas fa-arrow-up"></i>';
scrollToTopBtn.setAttribute('aria-label', 'Scroll to top');
document.body.appendChild(scrollToTopBtn);
const handleScroll = TW.utils.throttle(() => {
if (window.pageYOffset > 300) {
scrollToTopBtn.classList.remove('opacity-0', 'pointer-events-none');
scrollToTopBtn.classList.add('opacity-100');
} else {
scrollToTopBtn.classList.add('opacity-0', 'pointer-events-none');
scrollToTopBtn.classList.remove('opacity-100');
}
}, 100);
window.addEventListener('scroll', handleScroll);
scrollToTopBtn.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
};
// Enhanced form handling
TW.forms = {
init: function() {
this.setupFormValidation();
this.setupFormAnimations();
this.setupFileUploads();
},
setupFormValidation: function() {
const forms = document.querySelectorAll('form[data-validate]');
forms.forEach(form => {
form.addEventListener('submit', (e) => {
if (!this.validateForm(form)) {
e.preventDefault();
TW.animations.shake(form);
}
});
// Real-time validation
const inputs = form.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
input.addEventListener('blur', () => {
this.validateField(input);
});
});
});
},
validateForm: function(form) {
let isValid = true;
const inputs = form.querySelectorAll('input[required], textarea[required], select[required]');
inputs.forEach(input => {
if (!this.validateField(input)) {
isValid = false;
}
});
return isValid;
},
validateField: function(field) {
const value = field.value.trim();
const isRequired = field.hasAttribute('required');
const type = field.type;
let isValid = true;
let errorMessage = '';
// Required validation
if (isRequired && !value) {
isValid = false;
errorMessage = 'This field is required';
}
// Type-specific validation
if (value && type === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
isValid = false;
errorMessage = 'Please enter a valid email address';
}
}
// Update field appearance
this.updateFieldValidation(field, isValid, errorMessage);
return isValid;
},
updateFieldValidation: function(field, isValid, errorMessage) {
const fieldGroup = field.closest('.form-group');
if (!fieldGroup) return;
// Remove existing error states
field.classList.remove('form-input-error');
const existingError = fieldGroup.querySelector('.form-error');
if (existingError) {
existingError.remove();
}
if (!isValid) {
field.classList.add('form-input-error');
const errorElement = document.createElement('div');
errorElement.className = 'form-error';
errorElement.textContent = errorMessage;
fieldGroup.appendChild(errorElement);
TW.animations.slideInUp(errorElement, 200);
}
}
};
// Initialize all modules
TW.init = function() {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', this.initModules.bind(this));
} else {
this.initModules();
}
};
TW.initModules = function() {
console.log('🎢 ThrillWiki Enhanced JavaScript initialized');
// Initialize all modules
TW.search.init();
TW.cards.init();
TW.notifications.init();
TW.scroll.init();
TW.forms.init();
// Setup HTMX enhancements
if (window.htmx) {
this.setupHTMXEnhancements();
}
// Setup global error handling
this.setupErrorHandling();
};
TW.setupHTMXEnhancements = function() {
// Global HTMX configuration
htmx.config.globalViewTransitions = true;
htmx.config.scrollBehavior = 'smooth';
// Enhanced loading states
document.addEventListener('htmx:beforeRequest', (e) => {
const target = e.target;
target.classList.add('htmx-request');
});
document.addEventListener('htmx:afterRequest', (e) => {
const target = e.target;
target.classList.remove('htmx-request');
});
// Re-initialize components after HTMX swaps
document.addEventListener('htmx:afterSwap', (e) => {
// Re-initialize cards in the swapped content
const newCards = e.detail.target.querySelectorAll('.card-park, .card-ride, .card-feature');
if (newCards.length > 0) {
TW.cards.setupCardHovers();
}
// Re-initialize forms
const newForms = e.detail.target.querySelectorAll('form[data-validate]');
if (newForms.length > 0) {
TW.forms.setupFormValidation();
}
});
};
TW.setupErrorHandling = function() {
window.addEventListener('error', (e) => {
console.error('ThrillWiki Error:', e.error);
// Could send to error tracking service here
});
window.addEventListener('unhandledrejection', (e) => {
console.error('ThrillWiki Promise Rejection:', e.reason);
// Could send to error tracking service here
});
};
// Auto-initialize
TW.init();
})(window.ThrillWiki);
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = window.ThrillWiki;
}