mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 09:51:09 -05:00
feat: Implement UI components for Django templates
- Added Button component with various styles and sizes. - Introduced Card component for displaying content with titles and descriptions. - Created Input component for form fields with support for various attributes. - Developed Toast Notification Container for displaying alerts and messages. - Designed pages for listing designers and operators with pagination and responsive layout. - Documented frontend migration from React to HTMX + Alpine.js, detailing component usage and integration.
This commit is contained in:
@@ -33,11 +33,15 @@
|
||||
<!-- Alpine.js -->
|
||||
<script defer src="{% static 'js/alpine.min.js' %}"></script>
|
||||
|
||||
<!-- Alpine.js Components -->
|
||||
<script src="{% static 'js/alpine-components.js' %}"></script>
|
||||
|
||||
<!-- Location Autocomplete -->
|
||||
<script src="{% static 'js/location-autocomplete.js' %}"></script>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'css/components.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'css/alerts.css' %}" rel="stylesheet" />
|
||||
|
||||
<!-- Font Awesome -->
|
||||
@@ -77,201 +81,8 @@
|
||||
<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"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="sticky top-0 z-40 border-b shadow-lg bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg border-gray-200/50 dark:border-gray-700/50"
|
||||
>
|
||||
<nav class="container mx-auto nav-container">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center">
|
||||
<a
|
||||
href="{% url 'home' %}"
|
||||
class="font-bold text-transparent transition-transform site-logo bg-gradient-to-r from-primary to-secondary bg-clip-text hover:scale-105"
|
||||
>
|
||||
ThrillWiki
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links (Always Visible) -->
|
||||
<div class="flex items-center space-x-2 sm:space-x-4">
|
||||
<a href="{% url 'parks:park_list' %}" class="nav-link">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>Parks</span>
|
||||
</a>
|
||||
<a href="{% url 'rides:global_ride_list' %}" class="nav-link">
|
||||
<i class="fas fa-rocket"></i>
|
||||
<span>Rides</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="flex-1 hidden max-w-md mx-8 lg:flex">
|
||||
<form action="{% url 'search:search' %}" method="get" class="w-full">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
placeholder="Search parks and rides..."
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Right Side Menu -->
|
||||
<div class="flex items-center space-x-2 sm:space-x-6">
|
||||
<!-- Theme Toggle -->
|
||||
<label for="theme-toggle" class="cursor-pointer">
|
||||
<input type="checkbox" id="theme-toggle" class="hidden" />
|
||||
<div
|
||||
class="inline-flex items-center justify-center p-2 text-gray-500 transition-colors hover:text-primary dark:text-gray-400 dark:hover:text-primary theme-toggle-btn"
|
||||
role="button"
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
<i class="text-xl fas"></i>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- User Menu -->
|
||||
{% if user.is_authenticated %} {% if has_moderation_access %}
|
||||
<a href="{% url 'moderation:dashboard' %}" class="nav-link">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<span>Moderation</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div
|
||||
class="relative"
|
||||
x-data="{ open: false }"
|
||||
@click.outside="open = false"
|
||||
>
|
||||
<!-- Profile Picture Button -->
|
||||
{% if user.profile.avatar %}
|
||||
<img
|
||||
@click="open = !open"
|
||||
src="{{ user.profile.avatar.url }}"
|
||||
alt="{{ user.username }}"
|
||||
class="w-8 h-8 transition-transform rounded-full cursor-pointer ring-2 ring-primary/20 hover:scale-105"
|
||||
/>
|
||||
{% else %}
|
||||
<div
|
||||
@click="open = !open"
|
||||
class="flex items-center justify-center w-8 h-8 text-white transition-transform rounded-full cursor-pointer bg-gradient-to-br from-primary to-secondary hover:scale-105"
|
||||
>
|
||||
{{ user.username.0|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div
|
||||
x-cloak
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="bg-white dropdown-menu dark:bg-gray-800"
|
||||
>
|
||||
<a href="{% url 'profile' user.username %}" class="menu-item">
|
||||
<i class="w-5 fas fa-user"></i>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
<a href="{% url 'settings' %}" class="menu-item">
|
||||
<i class="w-5 fas fa-cog"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
{% if has_admin_access %}
|
||||
<a href="{% url 'admin:index' %}" class="menu-item">
|
||||
<i class="w-5 fas fa-shield-alt"></i>
|
||||
<span>Admin</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<form method="post" action="{% url 'account_logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="w-full menu-item">
|
||||
<i class="w-5 fas fa-sign-out-alt"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Generic Profile Icon for Unauthenticated Users -->
|
||||
<div
|
||||
class="relative"
|
||||
x-data="{ open: false }"
|
||||
@click.outside="open = false"
|
||||
>
|
||||
<div
|
||||
@click="open = !open"
|
||||
class="flex items-center justify-center w-8 h-8 text-gray-500 transition-transform rounded-full cursor-pointer hover:text-primary dark:text-gray-400 dark:hover:text-primary hover:scale-105"
|
||||
>
|
||||
<i class="text-xl fas fa-user"></i>
|
||||
</div>
|
||||
|
||||
<!-- Auth Menu -->
|
||||
<div
|
||||
x-cloak
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="bg-white dropdown-menu dark:bg-gray-800"
|
||||
>
|
||||
<div
|
||||
hx-get="{% url 'account_login' %}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="cursor-pointer menu-item"
|
||||
>
|
||||
<i class="w-5 fas fa-sign-in-alt"></i>
|
||||
<span>Login</span>
|
||||
</div>
|
||||
<div
|
||||
hx-get="{% url 'account_signup' %}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="cursor-pointer menu-item"
|
||||
>
|
||||
<i class="w-5 fas fa-user-plus"></i>
|
||||
<span>Register</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
id="mobileMenuBtn"
|
||||
class="p-2 text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-400"
|
||||
aria-label="Toggle mobile menu"
|
||||
>
|
||||
<i class="text-2xl fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div id="mobileMenu">
|
||||
<div class="space-y-4">
|
||||
<!-- Search (Mobile) -->
|
||||
<form action="{% url 'search:search' %}" method="get" class="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
placeholder="Search parks and rides..."
|
||||
class="form-input"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<!-- Enhanced Header -->
|
||||
{% include 'components/layout/enhanced_header.html' %}
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% if messages %}
|
||||
@@ -316,9 +127,15 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Global Auth Modal -->
|
||||
{% include 'components/auth/auth-modal.html' %}
|
||||
|
||||
<!-- Global Toast Container -->
|
||||
{% include 'components/ui/toast-container.html' %}
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
<script src="{% static 'js/alerts.js' %}"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
367
backend/templates/components/auth/auth-modal.html
Normal file
367
backend/templates/components/auth/auth-modal.html
Normal file
@@ -0,0 +1,367 @@
|
||||
{% comment %}
|
||||
Enhanced Authentication Modal Component
|
||||
Matches React frontend AuthDialog functionality with modal-based auth
|
||||
{% endcomment %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load account socialaccount %}
|
||||
|
||||
<!-- Auth Modal Component -->
|
||||
<div
|
||||
x-data="authModal()"
|
||||
x-show="open"
|
||||
x-cloak
|
||||
x-init="window.authModal = $data"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
@keydown.escape.window="close()"
|
||||
>
|
||||
<!-- Modal Overlay -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition-opacity ease-linear duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition-opacity ease-linear duration-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 bg-background/80 backdrop-blur-sm"
|
||||
@click="close()"
|
||||
></div>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="relative w-full max-w-md mx-4 bg-background border rounded-lg shadow-lg"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
@click="close()"
|
||||
class="absolute top-4 right-4 p-2 text-muted-foreground hover:text-foreground rounded-md hover:bg-accent transition-colors"
|
||||
>
|
||||
<i class="fas fa-times w-4 h-4"></i>
|
||||
</button>
|
||||
|
||||
<!-- Login Form -->
|
||||
<div x-show="mode === 'login'" class="p-6">
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-700">
|
||||
Sign In
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground mt-2">
|
||||
Enter your credentials to access your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Social Login Buttons -->
|
||||
<div x-show="socialProviders.length > 0" class="mb-6">
|
||||
<div class="grid grid-cols-2 gap-4" x-show="!socialLoading">
|
||||
<template x-for="provider in socialProviders" :key="provider.id">
|
||||
<button
|
||||
@click="handleSocialLogin(provider.id)"
|
||||
class="flex items-center justify-center px-4 py-2 text-sm font-medium text-white rounded-md transition-colors"
|
||||
:class="{
|
||||
'bg-[#4285F4] hover:bg-[#357AE8]': provider.id === 'google',
|
||||
'bg-[#5865F2] hover:bg-[#4752C4]': provider.id === 'discord',
|
||||
'bg-primary hover:bg-primary/90': !['google', 'discord'].includes(provider.id)
|
||||
}"
|
||||
>
|
||||
<i
|
||||
class="mr-2 w-4 h-4"
|
||||
:class="{
|
||||
'fab fa-google': provider.id === 'google',
|
||||
'fab fa-discord': provider.id === 'discord'
|
||||
}"
|
||||
></i>
|
||||
<span x-text="provider.name"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div x-show="socialLoading" class="grid grid-cols-2 gap-4">
|
||||
<div class="h-10 bg-muted animate-pulse rounded-md"></div>
|
||||
<div class="h-10 bg-muted animate-pulse rounded-md"></div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="relative my-6">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-muted"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="bg-background px-2 text-muted-foreground">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form
|
||||
@submit.prevent="handleLogin()"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<label for="login-username" class="text-sm font-medium">
|
||||
Email or Username
|
||||
</label>
|
||||
<input
|
||||
id="login-username"
|
||||
type="text"
|
||||
x-model="loginForm.username"
|
||||
placeholder="Enter your email or username"
|
||||
class="input w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="login-password" class="text-sm font-medium">
|
||||
Password
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="login-password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
x-model="loginForm.password"
|
||||
placeholder="Enter your password"
|
||||
class="input w-full pr-10"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<a
|
||||
href="{% url 'account_reset_password' %}"
|
||||
class="text-sm text-primary hover:text-primary/80 underline-offset-4 hover:underline font-medium"
|
||||
>
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
<div x-show="loginError" class="p-3 text-sm text-destructive-foreground bg-destructive/10 border border-destructive/20 rounded-md">
|
||||
<span x-text="loginError"></span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loginLoading"
|
||||
class="btn btn-default w-full bg-gradient-to-r from-blue-600 to-purple-700 hover:from-blue-700 hover:to-purple-800 text-white"
|
||||
>
|
||||
<span x-show="!loginLoading">Sign In</span>
|
||||
<span x-show="loginLoading" class="flex items-center">
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>
|
||||
Signing in...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Switch to Register -->
|
||||
<div class="text-center text-sm text-muted-foreground mt-6">
|
||||
Don't have an account?
|
||||
<button
|
||||
@click="switchToRegister()"
|
||||
class="text-primary hover:underline font-medium ml-1"
|
||||
type="button"
|
||||
>
|
||||
Sign up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Form -->
|
||||
<div x-show="mode === 'register'" class="p-6">
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-700">
|
||||
Create Account
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground mt-2">
|
||||
Join ThrillWiki to start exploring theme parks
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Social Registration Buttons -->
|
||||
<div x-show="socialProviders.length > 0" class="mb-6">
|
||||
<div class="grid grid-cols-2 gap-4" x-show="!socialLoading">
|
||||
<template x-for="provider in socialProviders" :key="provider.id">
|
||||
<button
|
||||
@click="handleSocialLogin(provider.id)"
|
||||
class="flex items-center justify-center px-4 py-2 text-sm font-medium text-white rounded-md transition-colors"
|
||||
:class="{
|
||||
'bg-[#4285F4] hover:bg-[#357AE8]': provider.id === 'google',
|
||||
'bg-[#5865F2] hover:bg-[#4752C4]': provider.id === 'discord',
|
||||
'bg-primary hover:bg-primary/90': !['google', 'discord'].includes(provider.id)
|
||||
}"
|
||||
>
|
||||
<i
|
||||
class="mr-2 w-4 h-4"
|
||||
:class="{
|
||||
'fab fa-google': provider.id === 'google',
|
||||
'fab fa-discord': provider.id === 'discord'
|
||||
}"
|
||||
></i>
|
||||
<span x-text="provider.name"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="relative my-6">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-muted"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="bg-background px-2 text-muted-foreground">
|
||||
Or continue with email
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Form -->
|
||||
<form
|
||||
@submit.prevent="handleRegister()"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<label for="register-first-name" class="text-sm font-medium">
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
id="register-first-name"
|
||||
type="text"
|
||||
x-model="registerForm.first_name"
|
||||
placeholder="First name"
|
||||
class="input w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label for="register-last-name" class="text-sm font-medium">
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
id="register-last-name"
|
||||
type="text"
|
||||
x-model="registerForm.last_name"
|
||||
placeholder="Last name"
|
||||
class="input w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="register-email" class="text-sm font-medium">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="register-email"
|
||||
type="email"
|
||||
x-model="registerForm.email"
|
||||
placeholder="Enter your email"
|
||||
class="input w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="register-username" class="text-sm font-medium">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="register-username"
|
||||
type="text"
|
||||
x-model="registerForm.username"
|
||||
placeholder="Choose a username"
|
||||
class="input w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="register-password" class="text-sm font-medium">
|
||||
Password
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="register-password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
x-model="registerForm.password1"
|
||||
placeholder="Create a password"
|
||||
class="input w-full pr-10"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="register-password2" class="text-sm font-medium">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
id="register-password2"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
x-model="registerForm.password2"
|
||||
placeholder="Confirm your password"
|
||||
class="input w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
<div x-show="registerError" class="p-3 text-sm text-destructive-foreground bg-destructive/10 border border-destructive/20 rounded-md">
|
||||
<span x-text="registerError"></span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="registerLoading"
|
||||
class="btn btn-default w-full bg-gradient-to-r from-blue-600 to-purple-700 hover:from-blue-700 hover:to-purple-800 text-white"
|
||||
>
|
||||
<span x-show="!registerLoading">Create Account</span>
|
||||
<span x-show="registerLoading" class="flex items-center">
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>
|
||||
Creating account...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Switch to Login -->
|
||||
<div class="text-center text-sm text-muted-foreground mt-6">
|
||||
Already have an account?
|
||||
<button
|
||||
@click="switchToLogin()"
|
||||
class="text-primary hover:underline font-medium ml-1"
|
||||
type="button"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
448
backend/templates/components/layout/enhanced_header.html
Normal file
448
backend/templates/components/layout/enhanced_header.html
Normal file
@@ -0,0 +1,448 @@
|
||||
{% comment %}
|
||||
Enhanced Header Component - Matches React Frontend Design
|
||||
Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
|
||||
{% endcomment %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
<header class="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div class="flex h-14 items-center justify-between px-4 max-w-full">
|
||||
|
||||
<!-- Logo and Browse Menu -->
|
||||
<div class="flex items-center space-x-6">
|
||||
<!-- Logo -->
|
||||
<a href="{% url 'home' %}" class="flex items-center space-x-2 flex-shrink-0">
|
||||
<div class="w-6 h-6 bg-purple-600 rounded flex items-center justify-center">
|
||||
<span class="text-white text-xs font-bold">TW</span>
|
||||
</div>
|
||||
<span class="font-bold text-lg">ThrillWiki</span>
|
||||
</a>
|
||||
|
||||
<!-- Browse Menu (Desktop) -->
|
||||
<div class="hidden md:block">
|
||||
<div
|
||||
x-data="{ open: false }"
|
||||
@mouseenter="open = true"
|
||||
@mouseleave="open = false"
|
||||
class="relative"
|
||||
>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md hover:bg-accent transition-colors"
|
||||
@click="open = !open"
|
||||
>
|
||||
<i class="fas fa-compass w-4 h-4"></i>
|
||||
Browse
|
||||
<i class="fas fa-chevron-down w-4 h-4"></i>
|
||||
</button>
|
||||
|
||||
<!-- Browse Dropdown -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
x-cloak
|
||||
class="absolute left-0 mt-2 w-[480px] p-6 bg-background border rounded-lg shadow-lg z-50"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-4">
|
||||
<a
|
||||
href="{% url 'parks:park_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-lg hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-map-marker-alt w-5 h-5 mt-0.5 text-muted-foreground group-hover:text-foreground"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm mb-1">Parks</h3>
|
||||
<p class="text-xs text-muted-foreground">Explore theme parks worldwide</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{% url 'rides:manufacturer_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-lg hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-wrench w-5 h-5 mt-0.5 text-muted-foreground group-hover:text-foreground"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm mb-1">Manufacturers</h3>
|
||||
<p class="text-xs text-muted-foreground">Ride and attraction manufacturers</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{% url 'parks:operator_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-lg hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-users w-5 h-5 mt-0.5 text-muted-foreground group-hover:text-foreground"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm mb-1">Operators</h3>
|
||||
<p class="text-xs text-muted-foreground">Theme park operating companies</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-4">
|
||||
<a
|
||||
href="{% url 'rides:global_ride_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-lg hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-rocket w-5 h-5 mt-0.5 text-muted-foreground group-hover:text-foreground"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm mb-1">Rides</h3>
|
||||
<p class="text-xs text-muted-foreground">Discover rides and attractions</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{% url 'rides:designer_list' %}"
|
||||
class="flex items-start gap-3 p-3 rounded-lg hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-drafting-compass w-5 h-5 mt-0.5 text-muted-foreground group-hover:text-foreground"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm mb-1">Designers</h3>
|
||||
<p class="text-xs text-muted-foreground">Ride designers and architects</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-start gap-3 p-3 rounded-lg hover:bg-accent transition-colors group"
|
||||
@click="open = false"
|
||||
>
|
||||
<i class="fas fa-trophy w-5 h-5 mt-0.5 text-muted-foreground group-hover:text-foreground"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm mb-1">Top Lists</h3>
|
||||
<p class="text-xs text-muted-foreground">Community rankings and favorites</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Right Side -->
|
||||
<div class="hidden md:flex items-center space-x-4">
|
||||
<!-- Enhanced Search -->
|
||||
<div class="relative" x-data="searchComponent()">
|
||||
<div class="relative">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"></i>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search parks, rides..."
|
||||
class="w-[300px] pl-10 pr-20 h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
x-model="query"
|
||||
@input.debounce.300ms="search()"
|
||||
hx-get="{% url 'search:search' %}"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
hx-target="#search-results"
|
||||
hx-include="this"
|
||||
name="q"
|
||||
/>
|
||||
{% include 'components/ui/button.html' with variant='default' size='sm' text='Search' class='absolute right-1 top-1/2 transform -translate-y-1/2' %}
|
||||
</div>
|
||||
|
||||
<!-- Search Results Dropdown -->
|
||||
<div
|
||||
id="search-results"
|
||||
x-show="results.length > 0"
|
||||
x-transition
|
||||
x-cloak
|
||||
class="absolute top-full left-0 right-0 mt-1 bg-background border rounded-md shadow-lg z-50 max-h-96 overflow-y-auto"
|
||||
>
|
||||
<!-- Search results will be populated by HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<div x-data="themeToggle()">
|
||||
<button
|
||||
@click="toggleTheme()"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-10 w-10"
|
||||
>
|
||||
<i class="fas fa-sun h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"></i>
|
||||
<i class="fas fa-moon absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"></i>
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- User Menu -->
|
||||
{% if user.is_authenticated %}
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||
<button @click="open = !open" class="relative h-8 w-8 rounded-full">
|
||||
{% if user.profile.avatar %}
|
||||
<img
|
||||
src="{{ user.profile.avatar.url }}"
|
||||
alt="{{ user.get_full_name|default:user.username }}"
|
||||
class="h-8 w-8 rounded-full object-cover"
|
||||
/>
|
||||
{% else %}
|
||||
<div class="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
|
||||
{{ user.get_full_name.0|default:user.username.0|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
x-cloak
|
||||
class="absolute right-0 mt-2 w-56 bg-background border rounded-md shadow-lg z-50"
|
||||
>
|
||||
<div class="flex items-center justify-start gap-2 p-2">
|
||||
<div class="flex flex-col space-y-1 leading-none">
|
||||
<p class="font-medium">{{ user.get_full_name|default:user.username }}</p>
|
||||
<p class="w-[200px] truncate text-sm text-muted-foreground">{{ user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t"></div>
|
||||
<a href="{% url 'profile' user.username %}" class="flex items-center px-2 py-2 text-sm hover:bg-accent">
|
||||
<i class="fas fa-user mr-2 h-4 w-4"></i>
|
||||
Profile
|
||||
</a>
|
||||
<a href="{% url 'settings' %}" class="flex items-center px-2 py-2 text-sm hover:bg-accent">
|
||||
<i class="fas fa-cog mr-2 h-4 w-4"></i>
|
||||
Settings
|
||||
</a>
|
||||
{% if has_moderation_access %}
|
||||
<a href="{% url 'moderation:dashboard' %}" class="flex items-center px-2 py-2 text-sm hover:bg-accent">
|
||||
<i class="fas fa-shield-alt mr-2 h-4 w-4"></i>
|
||||
Moderation
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="border-t"></div>
|
||||
<form method="post" action="{% url 'account_logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="flex items-center w-full px-2 py-2 text-sm text-red-600 hover:bg-accent">
|
||||
<i class="fas fa-sign-out-alt mr-2 h-4 w-4"></i>
|
||||
Log out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="window.authModal.show('login')"
|
||||
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 rounded-md px-3"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
<button
|
||||
@click="window.authModal.show('register')"
|
||||
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-9 rounded-md px-3"
|
||||
>
|
||||
Sign Up
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div class="md:hidden flex items-center space-x-2 flex-shrink-0">
|
||||
<!-- Theme Toggle (Mobile) -->
|
||||
<div x-data="themeToggle()">
|
||||
<button
|
||||
@click="toggleTheme()"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-10 w-10"
|
||||
>
|
||||
<i class="fas fa-sun h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"></i>
|
||||
<i class="fas fa-moon absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile User Menu -->
|
||||
{% if user.is_authenticated %}
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||
<button @click="open = !open" class="relative h-8 w-8 rounded-full">
|
||||
{% if user.profile.avatar %}
|
||||
<img
|
||||
src="{{ user.profile.avatar.url }}"
|
||||
alt="{{ user.get_full_name|default:user.username }}"
|
||||
class="h-8 w-8 rounded-full object-cover"
|
||||
/>
|
||||
{% else %}
|
||||
<div class="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
|
||||
{{ user.get_full_name.0|default:user.username.0|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
||||
<!-- Mobile User Dropdown -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition
|
||||
x-cloak
|
||||
class="absolute right-0 mt-2 w-56 bg-background border rounded-md shadow-lg z-50"
|
||||
>
|
||||
<div class="flex items-center justify-start gap-2 p-2">
|
||||
<div class="flex flex-col space-y-1 leading-none">
|
||||
<p class="font-medium">{{ user.get_full_name|default:user.username }}</p>
|
||||
<p class="w-[200px] truncate text-sm text-muted-foreground">{{ user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t"></div>
|
||||
<form method="post" action="{% url 'account_logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="flex items-center w-full px-2 py-2 text-sm text-red-600 hover:bg-accent">
|
||||
<i class="fas fa-sign-out-alt mr-2 h-4 w-4"></i>
|
||||
Log out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center space-x-1">
|
||||
<div
|
||||
hx-get="{% url 'account_login' %}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
{% include 'components/ui/button.html' with variant='outline' size='sm' text='Login' %}
|
||||
</div>
|
||||
<div
|
||||
hx-get="{% url 'account_signup' %}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
{% include 'components/ui/button.html' with variant='default' size='sm' text='Join' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div x-data="{ open: false }">
|
||||
<button
|
||||
@click="open = !open"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-10 w-10"
|
||||
>
|
||||
<i class="fas fa-bars h-5 w-5"></i>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Menu Overlay -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition-opacity ease-linear duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition-opacity ease-linear duration-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
x-cloak
|
||||
class="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
@click="open = false"
|
||||
>
|
||||
<!-- Mobile Menu Panel -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-in-out duration-300 transform"
|
||||
x-transition:enter-start="translate-x-full"
|
||||
x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transition ease-in-out duration-300 transform"
|
||||
x-transition:leave-start="translate-x-0"
|
||||
x-transition:leave-end="translate-x-full"
|
||||
class="fixed right-0 top-0 h-full w-full sm:w-96 bg-background border-l shadow-lg"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Mobile Menu Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-6 h-6 bg-purple-600 rounded flex items-center justify-center">
|
||||
<span class="text-white text-xs font-bold">TW</span>
|
||||
</div>
|
||||
<span class="font-bold text-lg">ThrillWiki</span>
|
||||
</div>
|
||||
<button
|
||||
@click="open = false"
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-10 w-10"
|
||||
>
|
||||
<i class="fas fa-times h-5 w-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Content -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Navigate through the ultimate theme park database
|
||||
</p>
|
||||
|
||||
<!-- Navigation Section -->
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||
NAVIGATION
|
||||
</h3>
|
||||
<div class="space-y-1">
|
||||
<a href="{% url 'home' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-home w-4 h-4"></i>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a href="{% url 'search:search' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-search w-4 h-4"></i>
|
||||
<span>Search</span>
|
||||
</a>
|
||||
<a href="{% url 'parks:park_list' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-map-marker-alt w-4 h-4"></i>
|
||||
<span>Parks</span>
|
||||
</a>
|
||||
<a href="{% url 'rides:global_ride_list' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-rocket w-4 h-4"></i>
|
||||
<span>Rides</span>
|
||||
</a>
|
||||
<a href="{% url 'rides:manufacturer_list' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-wrench w-4 h-4"></i>
|
||||
<span>Manufacturers</span>
|
||||
</a>
|
||||
<a href="{% url 'parks:operator_list' %}" class="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent transition-colors" @click="open = false">
|
||||
<i class="fas fa-building w-4 h-4"></i>
|
||||
<span>Operators</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Search Bar -->
|
||||
<div class="md:hidden border-t bg-background">
|
||||
<div class="px-4 py-3">
|
||||
<div class="relative">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"></i>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search parks, rides..."
|
||||
class="w-full pl-10 pr-20 h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
hx-get="{% url 'search:search' %}"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
hx-target="#mobile-search-results"
|
||||
hx-include="this"
|
||||
name="q"
|
||||
/>
|
||||
{% include 'components/ui/button.html' with variant='default' size='sm' text='Search' class='absolute right-1 top-1/2 transform -translate-y-1/2' %}
|
||||
</div>
|
||||
<div id="mobile-search-results" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
63
backend/templates/components/ui/button.html
Normal file
63
backend/templates/components/ui/button.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% comment %}
|
||||
Button Component - Django Template Version of shadcn/ui Button
|
||||
Usage: {% include 'components/ui/button.html' with variant='default' size='default' text='Click me' %}
|
||||
{% endcomment %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% with variant=variant|default:'default' size=size|default:'default' %}
|
||||
<button
|
||||
class="
|
||||
inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium
|
||||
ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2
|
||||
focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50
|
||||
{% if variant == 'default' %}
|
||||
bg-primary text-primary-foreground hover:bg-primary/90
|
||||
{% elif variant == 'destructive' %}
|
||||
bg-destructive text-destructive-foreground hover:bg-destructive/90
|
||||
{% elif variant == 'outline' %}
|
||||
border border-input bg-background hover:bg-accent hover:text-accent-foreground
|
||||
{% elif variant == 'secondary' %}
|
||||
bg-secondary text-secondary-foreground hover:bg-secondary/80
|
||||
{% elif variant == 'ghost' %}
|
||||
hover:bg-accent hover:text-accent-foreground
|
||||
{% elif variant == 'link' %}
|
||||
text-primary underline-offset-4 hover:underline
|
||||
{% endif %}
|
||||
{% if size == 'default' %}
|
||||
h-10 px-4 py-2
|
||||
{% elif size == 'sm' %}
|
||||
h-9 rounded-md px-3
|
||||
{% elif size == 'lg' %}
|
||||
h-11 rounded-md px-8
|
||||
{% elif size == 'icon' %}
|
||||
h-10 w-10
|
||||
{% endif %}
|
||||
{{ class|default:'' }}
|
||||
"
|
||||
{% if type %}type="{{ type }}"{% endif %}
|
||||
{% if onclick %}onclick="{{ onclick }}"{% endif %}
|
||||
{% if hx_get %}hx-get="{{ hx_get }}"{% endif %}
|
||||
{% if hx_post %}hx-post="{{ hx_post }}"{% endif %}
|
||||
{% if hx_target %}hx-target="{{ hx_target }}"{% endif %}
|
||||
{% if hx_swap %}hx-swap="{{ hx_swap }}"{% endif %}
|
||||
{% if x_data %}x-data="{{ x_data }}"{% endif %}
|
||||
{% if x_on %}{{ x_on }}{% endif %}
|
||||
{% if disabled %}disabled{% endif %}
|
||||
{{ attrs|default:'' }}
|
||||
>
|
||||
{% if icon_left %}
|
||||
<i class="{{ icon_left }} w-4 h-4"></i>
|
||||
{% endif %}
|
||||
|
||||
{% if text %}
|
||||
{{ text }}
|
||||
{% else %}
|
||||
{{ content|default:'' }}
|
||||
{% endif %}
|
||||
|
||||
{% if icon_right %}
|
||||
<i class="{{ icon_right }} w-4 h-4"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endwith %}
|
||||
37
backend/templates/components/ui/card.html
Normal file
37
backend/templates/components/ui/card.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% comment %}
|
||||
Card Component - Django Template Version of shadcn/ui Card
|
||||
Usage: {% include 'components/ui/card.html' with title='Card Title' content='Card content' %}
|
||||
{% endcomment %}
|
||||
|
||||
<div class="rounded-lg border bg-card text-card-foreground shadow-sm {{ class|default:'' }}">
|
||||
{% if title or header_content %}
|
||||
<div class="flex flex-col space-y-1.5 p-6">
|
||||
{% if title %}
|
||||
<h3 class="text-2xl font-semibold leading-none tracking-tight">{{ title }}</h3>
|
||||
{% endif %}
|
||||
{% if description %}
|
||||
<p class="text-sm text-muted-foreground">{{ description }}</p>
|
||||
{% endif %}
|
||||
{% if header_content %}
|
||||
{{ header_content|safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if content or body_content %}
|
||||
<div class="p-6 pt-0">
|
||||
{% if content %}
|
||||
{{ content|safe }}
|
||||
{% endif %}
|
||||
{% if body_content %}
|
||||
{{ body_content|safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if footer_content %}
|
||||
<div class="flex items-center p-6 pt-0">
|
||||
{{ footer_content|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
26
backend/templates/components/ui/input.html
Normal file
26
backend/templates/components/ui/input.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% comment %}
|
||||
Input Component - Django Template Version of shadcn/ui Input
|
||||
Usage: {% include 'components/ui/input.html' with type='text' placeholder='Enter text...' name='field_name' %}
|
||||
{% endcomment %}
|
||||
|
||||
<input
|
||||
type="{{ type|default:'text' }}"
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 {{ class|default:'' }}"
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
|
||||
{% if value %}value="{{ value }}"{% endif %}
|
||||
{% if required %}required{% endif %}
|
||||
{% if disabled %}disabled{% endif %}
|
||||
{% if readonly %}readonly{% endif %}
|
||||
{% if autocomplete %}autocomplete="{{ autocomplete }}"{% endif %}
|
||||
{% if x_model %}x-model="{{ x_model }}"{% endif %}
|
||||
{% if x_on %}{{ x_on }}{% endif %}
|
||||
{% if hx_get %}hx-get="{{ hx_get }}"{% endif %}
|
||||
{% if hx_post %}hx-post="{{ hx_post }}"{% endif %}
|
||||
{% if hx_target %}hx-target="{{ hx_target }}"{% endif %}
|
||||
{% if hx_trigger %}hx-trigger="{{ hx_trigger }}"{% endif %}
|
||||
{% if hx_swap %}hx-swap="{{ hx_swap }}"{% endif %}
|
||||
{% if hx_include %}hx-include="{{ hx_include }}"{% endif %}
|
||||
{{ attrs|default:'' }}
|
||||
/>
|
||||
90
backend/templates/components/ui/toast-container.html
Normal file
90
backend/templates/components/ui/toast-container.html
Normal file
@@ -0,0 +1,90 @@
|
||||
{% comment %}
|
||||
Toast Notification Container Component
|
||||
Matches React frontend toast functionality with Sonner-like behavior
|
||||
{% endcomment %}
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div
|
||||
x-data="toast()"
|
||||
x-show="$store.toast.toasts.length > 0"
|
||||
class="fixed top-4 right-4 z-50 space-y-2"
|
||||
x-cloak
|
||||
>
|
||||
<template x-for="toast in $store.toast.toasts" :key="toast.id">
|
||||
<div
|
||||
x-show="toast.visible"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="transform opacity-0 translate-x-full"
|
||||
x-transition:enter-end="transform opacity-100 translate-x-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="transform opacity-100 translate-x-0"
|
||||
x-transition:leave-end="transform opacity-0 translate-x-full"
|
||||
class="relative max-w-sm w-full bg-background border rounded-lg shadow-lg overflow-hidden"
|
||||
:class="{
|
||||
'border-green-200 bg-green-50 dark:bg-green-900/20 dark:border-green-800': toast.type === 'success',
|
||||
'border-red-200 bg-red-50 dark:bg-red-900/20 dark:border-red-800': toast.type === 'error',
|
||||
'border-yellow-200 bg-yellow-50 dark:bg-yellow-900/20 dark:border-yellow-800': toast.type === 'warning',
|
||||
'border-blue-200 bg-blue-50 dark:bg-blue-900/20 dark:border-blue-800': toast.type === 'info'
|
||||
}"
|
||||
>
|
||||
<!-- Progress Bar -->
|
||||
<div
|
||||
class="absolute top-0 left-0 h-1 bg-current opacity-30 transition-all duration-100 ease-linear"
|
||||
:style="`width: ${toast.progress}%`"
|
||||
:class="{
|
||||
'text-green-500': toast.type === 'success',
|
||||
'text-red-500': toast.type === 'error',
|
||||
'text-yellow-500': toast.type === 'warning',
|
||||
'text-blue-500': toast.type === 'info'
|
||||
}"
|
||||
></div>
|
||||
|
||||
<div class="p-4">
|
||||
<div class="flex items-start">
|
||||
<!-- Icon -->
|
||||
<div class="flex-shrink-0 mr-3">
|
||||
<i
|
||||
class="w-5 h-5"
|
||||
:class="{
|
||||
'fas fa-check-circle text-green-500': toast.type === 'success',
|
||||
'fas fa-exclamation-circle text-red-500': toast.type === 'error',
|
||||
'fas fa-exclamation-triangle text-yellow-500': toast.type === 'warning',
|
||||
'fas fa-info-circle text-blue-500': toast.type === 'info'
|
||||
}"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<!-- Message -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p
|
||||
class="text-sm font-medium"
|
||||
:class="{
|
||||
'text-green-800 dark:text-green-200': toast.type === 'success',
|
||||
'text-red-800 dark:text-red-200': toast.type === 'error',
|
||||
'text-yellow-800 dark:text-yellow-200': toast.type === 'warning',
|
||||
'text-blue-800 dark:text-blue-200': toast.type === 'info'
|
||||
}"
|
||||
x-text="toast.message"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<!-- Close Button -->
|
||||
<div class="flex-shrink-0 ml-3">
|
||||
<button
|
||||
@click="$store.toast.hide(toast.id)"
|
||||
class="inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors"
|
||||
:class="{
|
||||
'text-green-500 hover:bg-green-100 focus:ring-green-500 dark:hover:bg-green-800': toast.type === 'success',
|
||||
'text-red-500 hover:bg-red-100 focus:ring-red-500 dark:hover:bg-red-800': toast.type === 'error',
|
||||
'text-yellow-500 hover:bg-yellow-100 focus:ring-yellow-500 dark:hover:bg-yellow-800': toast.type === 'warning',
|
||||
'text-blue-500 hover:bg-blue-100 focus:ring-blue-500 dark:hover:bg-blue-800': toast.type === 'info'
|
||||
}"
|
||||
>
|
||||
<i class="fas fa-times w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Location Search - ThrillWiki{% endblock %}
|
||||
@@ -329,4 +329,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/location-search.js' %}"></script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
92
backend/templates/designers/designer_list.html
Normal file
92
backend/templates/designers/designer_list.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Ride Designers - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2">Ride Designers</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Discover the creative minds behind the world's most innovative attractions.
|
||||
{{ total_designers }} designer{{ total_designers|pluralize }} found.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for designer in designers %}
|
||||
<div class="bg-card rounded-lg border p-6 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-1">{{ designer.name }}</h3>
|
||||
{% if designer.founded_date %}
|
||||
<p class="text-sm text-muted-foreground">Founded {{ designer.founded_date.year }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||||
{{ designer.ride_count }} ride{{ designer.ride_count|pluralize }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if designer.description %}
|
||||
<p class="text-sm text-muted-foreground mb-4 line-clamp-3">
|
||||
{{ designer.description|truncatewords:20 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
{% if designer.website %}
|
||||
<a href="{{ designer.website }}" target="_blank" rel="noopener noreferrer"
|
||||
class="text-sm text-primary hover:underline">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Website
|
||||
</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
|
||||
<a href="#" class="text-sm text-primary hover:underline">
|
||||
View Rides →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-span-full text-center py-12">
|
||||
<i class="fas fa-drafting-compass text-4xl text-muted-foreground mb-4"></i>
|
||||
<h3 class="text-lg font-semibold mb-2">No designers found</h3>
|
||||
<p class="text-muted-foreground">There are no designers to display at this time.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="mt-8 flex justify-center">
|
||||
<nav class="flex items-center space-x-2">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
First
|
||||
</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-2 text-sm font-medium">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
Next
|
||||
</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
Last
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,63 +1,92 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Manufacturers - ThrillWiki{% endblock %}
|
||||
{% block title %}Ride Manufacturers - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container px-4 mx-auto sm:px-6 lg:px-8">
|
||||
<!-- Header -->
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Ride Manufacturers</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Companies that manufacture theme park rides and attractions</p>
|
||||
<h1 class="text-3xl font-bold mb-2">Ride Manufacturers</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Explore the companies that design and build the world's most thrilling rides.
|
||||
{{ total_manufacturers }} manufacturer{{ total_manufacturers|pluralize }} found.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Manufacturers List -->
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for manufacturer in manufacturers %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||
<a href="{% url 'manufacturers:manufacturer_detail' manufacturer.slug %}" class="hover:text-blue-600 dark:hover:text-blue-400">
|
||||
{{ manufacturer.name }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{% if manufacturer.description %}
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ manufacturer.description|truncatewords:20 }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="text-sm text-gray-500 dark:text-gray-500">
|
||||
{% if manufacturer.rides_count %}
|
||||
<span class="inline-block mr-4">{{ manufacturer.rides_count }} ride{{ manufacturer.rides_count|pluralize }}</span>
|
||||
{% endif %}
|
||||
{% if manufacturer.founded_year %}
|
||||
<span class="inline-block">Founded {{ manufacturer.founded_year }}</span>
|
||||
<div class="bg-card rounded-lg border p-6 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-1">{{ manufacturer.name }}</h3>
|
||||
{% if manufacturer.founded_date %}
|
||||
<p class="text-sm text-muted-foreground">Founded {{ manufacturer.founded_date.year }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||||
{{ manufacturer.ride_count }} ride{{ manufacturer.ride_count|pluralize }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if manufacturer.description %}
|
||||
<p class="text-sm text-muted-foreground mb-4 line-clamp-3">
|
||||
{{ manufacturer.description|truncatewords:20 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
{% if manufacturer.website %}
|
||||
<a href="{{ manufacturer.website }}" target="_blank" rel="noopener noreferrer"
|
||||
class="text-sm text-primary hover:underline">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Website
|
||||
</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
|
||||
<a href="#" class="text-sm text-primary hover:underline">
|
||||
View Rides →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-span-full text-center py-12">
|
||||
<p class="text-gray-500 dark:text-gray-400">No manufacturers found.</p>
|
||||
</div>
|
||||
<div class="col-span-full text-center py-12">
|
||||
<i class="fas fa-wrench text-4xl text-muted-foreground mb-4"></i>
|
||||
<h3 class="text-lg font-semibold mb-2">No manufacturers found</h3>
|
||||
<p class="text-muted-foreground">There are no manufacturers to display at this time.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="mt-8 flex justify-center">
|
||||
<nav class="flex space-x-2">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Next</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-center">
|
||||
<nav class="flex items-center space-x-2">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
First
|
||||
</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-2 text-sm font-medium">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
Next
|
||||
</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
Last
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
92
backend/templates/operators/operator_list.html
Normal file
92
backend/templates/operators/operator_list.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Park Operators - ThrillWiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2">Park Operators</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Explore the companies that own and operate theme parks around the world.
|
||||
{{ total_operators }} operator{{ total_operators|pluralize }} found.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for operator in operators %}
|
||||
<div class="bg-card rounded-lg border p-6 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-1">{{ operator.name }}</h3>
|
||||
{% if operator.founded_date %}
|
||||
<p class="text-sm text-muted-foreground">Founded {{ operator.founded_date.year }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||||
{{ operator.park_count }} park{{ operator.park_count|pluralize }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if operator.description %}
|
||||
<p class="text-sm text-muted-foreground mb-4 line-clamp-3">
|
||||
{{ operator.description|truncatewords:20 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
{% if operator.website %}
|
||||
<a href="{{ operator.website }}" target="_blank" rel="noopener noreferrer"
|
||||
class="text-sm text-primary hover:underline">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Website
|
||||
</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
|
||||
<a href="#" class="text-sm text-primary hover:underline">
|
||||
View Parks →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-span-full text-center py-12">
|
||||
<i class="fas fa-building text-4xl text-muted-foreground mb-4"></i>
|
||||
<h3 class="text-lg font-semibold mb-2">No operators found</h3>
|
||||
<p class="text-muted-foreground">There are no operators to display at this time.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="mt-8 flex justify-center">
|
||||
<nav class="flex items-center space-x-2">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
First
|
||||
</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-2 text-sm font-medium">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
Next
|
||||
</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}" class="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground">
|
||||
Last
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -18,7 +18,7 @@
|
||||
<a href="{% url 'parks:park_detail' park.slug %}" class="text-blue-600 hover:text-blue-800">← Back to {{ park.name }}</a>
|
||||
{% else %}
|
||||
<h1 class="mb-2 text-3xl font-bold">{{ category }}</h1>
|
||||
<a href="{% url 'rides:ride_list' %}" class="text-blue-600 hover:text-blue-800">← Back to All Rides</a>
|
||||
<a href="{% url 'rides:global_ride_list' %}" class="text-blue-600 hover:text-blue-800">← Back to All Rides</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<!-- Clear All Filters -->
|
||||
{% if has_filters %}
|
||||
<button type="button"
|
||||
hx-get="{% url 'rides:ride_list' %}"
|
||||
hx-get="{% url 'rides:global_ride_list' %}"
|
||||
hx-target="#filter-results"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<!-- Filter Form -->
|
||||
<form id="filter-form"
|
||||
hx-get="{% url 'rides:ride_list' %}"
|
||||
hx-get="{% url 'rides:global_ride_list' %}"
|
||||
hx-target="#filter-results"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="change, input delay:500ms"
|
||||
@@ -462,4 +462,4 @@ function updateFilterCounts() {
|
||||
badge.style.display = activeCount > 0 ? 'inline-flex' : 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<i class="fas fa-filter mr-2"></i>
|
||||
Active Filters ({{ active_filters|length }})
|
||||
</h3>
|
||||
<button hx-get="{% url 'rides:ride_list' %}{% if park %}?park={{ park.id }}{% endif %}"
|
||||
<button hx-get="{% url 'rides:global_ride_list' %}{% if park %}?park={{ park.id }}{% endif %}"
|
||||
hx-target="#filter-results"
|
||||
hx-push-url="true"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200 transition-colors">
|
||||
@@ -59,7 +59,7 @@
|
||||
<label for="sort-select" class="text-sm font-medium text-gray-700 dark:text-gray-300">Sort by:</label>
|
||||
<select id="sort-select"
|
||||
name="sort"
|
||||
hx-get="{% url 'rides:ride_list' %}"
|
||||
hx-get="{% url 'rides:global_ride_list' %}"
|
||||
hx-target="#filter-results"
|
||||
hx-include="[name='q'], [name='category'], [name='manufacturer'], [name='designer'], [name='min_height'], [name='max_height'], [name='min_speed'], [name='max_speed'], [name='min_capacity'], [name='max_capacity'], [name='min_duration'], [name='max_duration'], [name='opened_after'], [name='opened_before'], [name='closed_after'], [name='closed_before'], [name='operating_status'], [name='has_inversions'], [name='has_launches'], [name='track_type'], [name='min_inversions'], [name='max_inversions'], [name='min_launches'], [name='max_launches'], [name='min_top_speed'], [name='max_top_speed'], [name='min_max_height'], [name='max_max_height']{% if park %}, [name='park']{% endif %}"
|
||||
hx-push-url="true"
|
||||
@@ -306,7 +306,7 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if has_filters %}
|
||||
<button hx-get="{% url 'rides:ride_list' %}{% if park %}?park={{ park.id }}{% endif %}"
|
||||
<button hx-get="{% url 'rides:global_ride_list' %}{% if park %}?park={{ park.id }}{% endif %}"
|
||||
hx-target="#filter-results"
|
||||
hx-push-url="true"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 transition-colors">
|
||||
@@ -323,4 +323,4 @@
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">Loading results...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Manufacturer</dt>
|
||||
<dd class="mt-1">
|
||||
{% if ride.manufacturer %}
|
||||
<a href="{% url 'manufacturers:manufacturer_detail' ride.manufacturer.slug %}"
|
||||
<a href="#"
|
||||
class="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
{{ ride.manufacturer.name }}
|
||||
</a>
|
||||
@@ -360,7 +360,7 @@
|
||||
<dt class="text-gray-500 dark:text-gray-400">Manufacturer</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">
|
||||
{% if ride.manufacturer %}
|
||||
<a href="{% url 'manufacturers:manufacturer_detail' ride.manufacturer.slug %}"
|
||||
<a href="#"
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
{{ ride.manufacturer.name }}
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
@@ -255,4 +255,4 @@ if (localStorage.getItem('darkMode') === 'true' ||
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user