mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:51: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:
828
static/css/design-system.css
Normal file
828
static/css/design-system.css
Normal 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);
|
||||
}
|
||||
799
static/js/backup/thrillwiki-enhanced.js
Normal file
799
static/js/backup/thrillwiki-enhanced.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user