Restored to 'ba32d51b3eb6866667ec8382daca17202cf7da86'

Replit-Restored-To: ba32d51b3eb6866667ec8382daca17202cf7da86
This commit is contained in:
pac7
2025-09-21 14:10:27 +00:00
committed by pacnpal
parent 434ac4c641
commit 3cad7c5641
21 changed files with 30 additions and 1540 deletions

View File

@@ -1,5 +1,4 @@
{% load static %}
{% load cotton %}
<!DOCTYPE html>
<html lang="en">
<head>
@@ -128,11 +127,11 @@
</div>
</footer>
<!-- Global Auth Modal (Cotton Component) -->
<c-auth.modal />
<!-- Global Auth Modal -->
{% include 'components/auth/auth-modal.html' %}
<!-- Global Toast Container (Cotton Component) -->
<c-ui.toast />
<!-- Global Toast Container -->
{% include 'components/ui/toast-container.html' %}
<!-- Custom JavaScript with cache control -->
<script src="{% static 'js/main.js' %}?v={{ version|default:'1.0' }}"></script>

View File

@@ -4,7 +4,6 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
{% endcomment %}
{% load static %}
{% load cotton %}
<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">
@@ -150,7 +149,7 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
hx-include="this"
name="q"
/>
<c-ui.button variant="default" size="sm" text="Search" button_classes="absolute right-1 top-1/2 transform -translate-y-1/2" />
{% 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 -->
@@ -238,7 +237,7 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
</div>
</div>
{% else %}
<div class="hidden md:flex items-center space-x-2">
<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"
@@ -309,14 +308,14 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
</div>
</div>
{% else %}
<div class="hidden md:flex items-center space-x-1">
<div class="flex items-center space-x-1">
<div
hx-get="{% url 'account_login' %}"
hx-target="body"
hx-swap="beforeend"
class="cursor-pointer"
>
<c-ui.button variant="outline" size="sm" text="Login" />
{% include 'components/ui/button.html' with variant='outline' size='sm' text='Login' %}
</div>
<div
hx-get="{% url 'account_signup' %}"
@@ -324,13 +323,13 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
hx-swap="beforeend"
class="cursor-pointer"
>
<c-ui.button variant="default" size="sm" text="Join" />
{% include 'components/ui/button.html' with variant='default' size='sm' text='Join' %}
</div>
</div>
{% endif %}
<!-- Mobile Menu Button -->
<div class="md:hidden" x-data="{ open: false }">
<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"
@@ -386,39 +385,6 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
Navigate through the ultimate theme park database
</p>
<!-- Mobile Authentication -->
{% if not user.is_authenticated %}
<div class="bg-accent/30 rounded-lg p-4 border border-border">
<h3 class="text-sm font-medium text-foreground mb-3">
Get Started
</h3>
<div class="flex gap-2">
<c-ui.button
variant="outline"
size="lg"
text="Sign In"
icon_left="fas fa-sign-in-alt"
button_classes="flex-1 justify-center"
hx_get="{% url 'account_login' %}"
hx_target="body"
hx_swap="beforeend"
x_on="@click='open = false'"
/>
<c-ui.button
variant="default"
size="lg"
text="Join ThrillWiki"
icon_left="fas fa-user-plus"
button_classes="flex-1 justify-center"
hx_get="{% url 'account_signup' %}"
hx_target="body"
hx_swap="beforeend"
x_on="@click='open = false'"
/>
</div>
</div>
{% endif %}
<!-- Navigation Section -->
<div>
<h3 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
@@ -460,33 +426,23 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
</div>
<!-- Mobile Search Bar -->
<div class="md:hidden border-t border-border bg-background">
<div class="px-4 py-4">
<div class="bg-accent/30 rounded-lg p-3 border border-border">
<div class="flex gap-2">
<div class="relative flex-1">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"></i>
<c-ui.input
type="search"
placeholder="Search parks, rides..."
input_classes="pl-10 flex-1"
hx_get="{% url 'search:search' %}"
hx_trigger="input changed delay:300ms"
hx_target="#mobile-search-results"
hx_include="this"
name="q"
/>
</div>
<c-ui.button
variant="default"
size="default"
icon_left="fas fa-search"
button_classes="px-3 flex-shrink-0"
type="submit"
/>
</div>
<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-3"></div>
<div id="mobile-search-results" class="mt-2"></div>
</div>
</div>
</header>

View File

@@ -1,131 +0,0 @@
{% comment %}
Cotton Login Form Component
Converts existing login form to use Django Cotton's component system
{% endcomment %}
{% load i18n %}
{% load account socialaccount %}
{% load turnstile_tags %}
<!-- Cotton Login Form Component -->
<c-vars
form_classes="form_classes|default:'space-y-6'"
show_remember="show_remember|default:'true'"
show_forgot_password="show_forgot_password|default:'true'"
button_text="button_text|default:'Sign In'"
hx_target="hx_target|default:'this'"
hx_swap="hx_swap|default:'outerHTML'"
/>
<form
class="{{ form_classes }}"
hx-post="{% url 'account_login' %}"
hx-target="{{ hx_target }}"
hx-swap="{{ hx_swap }}"
hx-indicator="#login-indicator"
>
{% csrf_token %}
<!-- Form Errors -->
{% if form.non_field_errors %}
<div class="alert alert-error">
<div class="text-sm">{{ form.non_field_errors }}</div>
</div>
{% endif %}
<!-- Username/Email Field -->
<div>
<label for="id_login" class="form-label">
{% trans "Username or Email" %}
</label>
<input
type="text"
name="login"
id="id_login"
required
autocomplete="username email"
class="form-input"
/>
{% if form.login.errors %}
<p class="form-error">{{ form.login.errors }}</p>
{% endif %}
</div>
<!-- Password Field -->
<div>
<label for="id_password" class="form-label">
{% trans "Password" %}
</label>
<input
type="password"
name="password"
id="id_password"
required
autocomplete="current-password"
class="form-input"
/>
{% if form.password.errors %}
<p class="form-error">{{ form.password.errors }}</p>
{% endif %}
</div>
<!-- Remember Me & Forgot Password -->
<div class="flex items-center justify-between">
{% if show_remember and show_remember != '' %}
<div class="flex items-center">
<input
type="checkbox"
name="remember"
id="id_remember"
class="w-4 h-4 border-gray-300 rounded text-primary focus:ring-primary/50 dark:border-gray-700"
/>
<label
for="id_remember"
class="block ml-2 text-sm text-gray-700 dark:text-gray-300"
>
{% trans "Remember me" %}
</label>
</div>
{% endif %}
{% if show_forgot_password and show_forgot_password != '' %}
<div class="text-sm">
<a
href="{% url 'account_reset_password' %}"
class="font-medium transition-colors text-primary hover:text-primary/80 focus:outline-hidden focus:underline"
>
{% trans "Forgot Password?" %}
</a>
</div>
{% endif %}
</div>
<!-- Turnstile Widget -->
<c-auth.turnstile_widget site_key="{{ site_key|default:'' }}" />
<!-- Redirect Field -->
{% if redirect_field_value %}
<input
type="hidden"
name="{{ redirect_field_name }}"
value="{{ redirect_field_value }}"
/>
{% endif %}
<!-- Submit Button -->
<c-slot name="submit-button">
<div>
<button type="submit" class="w-full btn-primary">
<i class="mr-2 fas fa-sign-in-alt"></i>
{{ button_text }}
</button>
</div>
</c-slot>
</form>
<!-- Loading Indicator -->
<div id="login-indicator" class="htmx-indicator">
<div class="flex items-center justify-center w-full py-4">
<div class="w-8 h-8 border-4 rounded-full border-primary border-t-transparent animate-spin"></div>
</div>
</div>

View File

@@ -1,377 +0,0 @@
{% comment %}
Cotton Auth Modal Component
Converts the existing auth modal to use Django Cotton's component system
{% endcomment %}
{% load static %}
{% load i18n %}
{% load account socialaccount %}
<!-- Cotton Auth Modal Component -->
<c-vars
modal_classes="{{ modal_classes|default:'fixed inset-0 z-50 flex items-center justify-center' }}"
overlay_classes="{{ overlay_classes|default:'fixed inset-0 bg-background/80 backdrop-blur-sm' }}"
content_classes="{{ content_classes|default:'relative w-full max-w-md mx-4 bg-background border rounded-lg shadow-lg' }}"
/>
<div
x-data="authModal"
x-show="open"
x-cloak
x-init="window.authModal = $data"
class="{{ modal_classes }}"
@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="{{ overlay_classes }}"
@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="{{ content_classes }}"
@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 -->
<c-slot name="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>
</c-slot>
<!-- Register Form -->
<c-slot name="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>
</c-slot>
</div>
</div>

View File

@@ -1,17 +0,0 @@
{% comment %}
Cotton Turnstile Empty Widget Component
Empty template when DEBUG is True - converts to Cotton format
{% endcomment %}
<!-- Cotton Turnstile Empty Component -->
<c-vars
debug_message="debug_message|default:'Turnstile widget disabled in DEBUG mode'"
show_debug_message="show_debug_message|default:''"
/>
<!-- Empty template when DEBUG is True -->
{% if show_debug_message and show_debug_message != '' %}
<div class="text-xs text-gray-500 text-center py-2">
{{ debug_message }}
</div>
{% endif %}

View File

@@ -1,36 +0,0 @@
{% comment %}
Cotton Turnstile Widget Component
Converts existing Cloudflare Turnstile widget to use Django Cotton's component system
{% endcomment %}
<!-- Cotton Turnstile Widget Component -->
<c-vars
site_key="site_key"
widget_classes="widget_classes|default:'turnstile'"
widget_id="widget_id|default:'turnstile-widget'"
theme="theme|default:'auto'"
/>
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
></script>
<div class="{{ widget_classes }}">
<div
id="{{ widget_id }}"
class="cf-turnstile"
data-sitekey="{{ site_key }}"
></div>
</div>
<script>
// Apply theme to the Turnstile widget based on the retrieved theme
document.addEventListener("DOMContentLoaded", function () {
const turnstileWidget = document.getElementById("{{ widget_id }}");
if (turnstileWidget) {
turnstileWidget.setAttribute("data-theme", "{{ theme }}");
}
});
</script>

View File

@@ -1,82 +0,0 @@
{% comment %}
Cotton Button Component - Django Template Version of shadcn/ui Button
Converts existing button component to use Django Cotton's component system
{% endcomment %}
{% load static %}
<!-- Cotton Button Component -->
<c-vars
variant="variant|default:'default'"
size="size|default:'default'"
button_classes="button_classes|default:''"
type="type|default:'button'"
disabled="disabled|default:''"
onclick="onclick|default:''"
x_data="x_data|default:''"
x_on="x_on|default:''"
hx_get="hx_get|default:''"
hx_post="hx_post|default:''"
hx_target="hx_target|default:''"
hx_swap="hx_swap|default:''"
icon_left="icon_left|default:''"
icon_right="icon_right|default:''"
text="text|default:''"
attrs="attrs|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 %}
{{ button_classes }}
"
type="{{ type }}"
{% 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 and x_data != 'x_data' and x_data != '' %}x-data="{{ x_data }}"{% endif %}
{% if x_on %}{{ x_on }}{% endif %}
{% if disabled and disabled != '' %}disabled{% endif %}
{{ attrs }}
>
{% if icon_left %}
<i class="{{ icon_left }} w-4 h-4"></i>
{% endif %}
<!-- Button Text Content -->
<c-slot name="content">
{% if text %}
{{ text }}
{% endif %}
</c-slot>
{% if icon_right %}
<i class="{{ icon_right }} w-4 h-4"></i>
{% endif %}
</button>

View File

@@ -1,58 +0,0 @@
{% comment %}
Cotton Card Component - Django Template Version of shadcn/ui Card
Converts existing card component to use Django Cotton's component system
{% endcomment %}
<!-- Cotton Card Component -->
<c-vars
card_classes="card_classes|default:''"
title="title|default:''"
description="description|default:''"
header_content="header_content|default:''"
body_content="body_content|default:''"
footer_content="footer_content|default:''"
content="content|default:''"
/>
<div class="rounded-lg border bg-card text-card-foreground shadow-sm {{ card_classes }}">
{% if title or description 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 %}
<!-- Card Header Slot -->
<c-slot name="header">
{% if header_content %}
{{ header_content|safe }}
{% endif %}
</c-slot>
</div>
{% endif %}
{% if content or body_content %}
<div class="p-6 pt-0">
<!-- Card Body Content -->
<c-slot name="content">
{% if content %}
{{ content|safe }}
{% endif %}
{% if body_content %}
{{ body_content|safe }}
{% endif %}
</c-slot>
</div>
{% endif %}
{% if footer_content %}
<div class="flex items-center p-6 pt-0">
<!-- Card Footer Slot -->
<c-slot name="footer">
{{ footer_content|safe }}
</c-slot>
</div>
{% endif %}
</div>

View File

@@ -1,51 +0,0 @@
{% comment %}
Cotton Input Component - Django Template Version of shadcn/ui Input
Converts existing input component to use Django Cotton's component system
{% endcomment %}
<!-- Cotton Input Component -->
<c-vars
type="type|default:'text'"
placeholder="placeholder|default:''"
name="name|default:''"
value="value|default:''"
id="id|default:''"
input_classes="input_classes|default:''"
disabled="disabled|default:''"
required="required|default:''"
readonly="readonly|default:''"
x_model="x_model"
x_data="x_data"
x_on="x_on"
hx_get="hx_get|default:''"
hx_post="hx_post|default:''"
hx_target="hx_target|default:''"
hx_swap="hx_swap|default:''"
attrs="attrs|default:''"
/>
<input
type="{{ type }}"
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
{% if name %}name="{{ name }}"{% endif %}
{% if value %}value="{{ value }}"{% endif %}
{% if id %}id="{{ id }}"{% endif %}
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 {{ input_classes }}
"
{% if disabled %}disabled{% endif %}
{% if required %}required{% endif %}
{% if readonly %}readonly{% endif %}
{% if x_model %}x-model="{{ x_model }}"{% endif %}
{% if x_data and x_data != 'x_data' %}x-data="{{ x_data }}"{% 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_swap %}hx-swap="{{ hx_swap }}"{% endif %}
{{ attrs }}
/>

View File

@@ -1,111 +0,0 @@
{% comment %}
Cotton Pagination Component
Converts existing pagination component to use Django Cotton's component system
{% endcomment %}
<!-- Cotton Pagination Component -->
<c-vars
page_obj="page_obj"
pagination_classes="pagination_classes|default:''"
show_page_info="show_page_info|default:'true'"
/>
{% if page_obj %}
<div class="flex items-center justify-between px-2 {{ pagination_classes }}">
<!-- Mobile Navigation -->
<div class="flex-1 flex justify-between sm:hidden">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}"
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700">
Previous
</a>
{% else %}
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md dark:bg-gray-800 dark:text-gray-500 dark:border-gray-600">
Previous
</span>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}"
class="ml-3 relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700">
Next
</a>
{% else %}
<span class="ml-3 relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md dark:bg-gray-800 dark:text-gray-500 dark:border-gray-600">
Next
</span>
{% endif %}
</div>
<!-- Desktop Navigation -->
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<!-- Page Info (Optional) -->
{% if show_page_info and show_page_info != '' %}
<div>
<p class="text-sm text-gray-700 dark:text-gray-300">
Showing
<span class="font-medium">{{ page_obj.start_index }}</span>
to
<span class="font-medium">{{ page_obj.end_index }}</span>
of
<span class="font-medium">{{ page_obj.paginator.count }}</span>
results
</p>
</div>
{% else %}
<div></div>
{% endif %}
<!-- Page Numbers (Always Visible) -->
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<!-- Previous Page -->
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
<span class="sr-only">Previous</span>
<i class="fas fa-chevron-left h-5 w-5"></i>
</a>
{% else %}
<span class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600">
<span class="sr-only">Previous</span>
<i class="fas fa-chevron-left h-5 w-5"></i>
</span>
{% endif %}
<!-- Page Numbers Logic -->
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<span class="relative inline-flex items-center px-4 py-2 border border-blue-500 bg-blue-50 text-sm font-medium text-blue-600 dark:bg-blue-900 dark:text-blue-300 dark:border-blue-700">
{{ num }}
</span>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<a href="?page={{ num }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">
{{ num }}
</a>
{% elif num == page_obj.number|add:'-4' or num == page_obj.number|add:'4' %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300">
...
</span>
{% endif %}
{% endfor %}
<!-- Next Page -->
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
<span class="sr-only">Next</span>
<i class="fas fa-chevron-right h-5 w-5"></i>
</a>
{% else %}
<span class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600">
<span class="sr-only">Next</span>
<i class="fas fa-chevron-right h-5 w-5"></i>
</span>
{% endif %}
</nav>
</div>
</div>
</div>
{% endif %}

View File

@@ -1,158 +0,0 @@
{% comment %}
Cotton Search Form Component
Converts existing search form component to use Django Cotton's component system
Preserves accessibility and structure from original component
{% endcomment %}
<!-- Cotton Search Form Component -->
<c-vars
placeholder="placeholder|default:'Search...'"
filters="filters|default:''"
form_classes="form_classes|default:'bg-white p-6 rounded-lg shadow-sm border border-gray-200 mb-6'"
show_sort="show_sort|default:''"
sort_options="sort_options|default:''"
/>
<form method="get" class="{{ form_classes }}">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Search Input -->
<div class="col-span-1 md:col-span-2">
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">
Search
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
name="search"
id="search"
value="{{ request.GET.search }}"
placeholder="{{ placeholder }}"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
</div>
</div>
<!-- Filter Slots -->
<c-slot name="filters">
{% if filters %}
{% for filter in filters %}
<div>
<label for="{{ filter.name }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ filter.label }}
</label>
{% if filter.type == 'select' %}
<select
name="{{ filter.name }}"
id="{{ filter.name }}"
class="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">All {{ filter.label }}</option>
{% for option in filter.options %}
<option
value="{{ option.value }}"
{% if request.GET|get_item:filter.name == option.value %}selected{% endif %}
>
{{ option.label }}
</option>
{% endfor %}
</select>
{% elif filter.type == 'checkbox' %}
<div class="space-y-2">
{% for option in filter.options %}
<label class="flex items-center">
<input
type="checkbox"
name="{{ filter.name }}"
value="{{ option.value }}"
{% if option.value in request.GET|getlist:filter.name %}checked{% endif %}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
>
<span class="ml-2 text-sm text-gray-700">{{ option.label }}</span>
</label>
{% endfor %}
</div>
{% elif filter.type == 'range' %}
<div class="flex space-x-2">
<input
type="number"
name="{{ filter.name }}_min"
value="{{ request.GET|get_item:filter.name_min }}"
placeholder="Min"
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<input
type="number"
name="{{ filter.name }}_max"
value="{{ request.GET|get_item:filter.name_max }}"
placeholder="Max"
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
</c-slot>
</div>
<!-- Action Buttons -->
<div class="mt-4 flex items-center justify-between">
<div class="flex space-x-3">
<button
type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
Search
</button>
{% if request.GET.urlencode %}
<a
href="{{ request.path }}"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Clear
</a>
{% endif %}
</div>
<!-- Sort Options Slot -->
<c-slot name="sort-options">
{% if show_sort %}
<div class="flex items-center space-x-2">
<label for="ordering" class="text-sm font-medium text-gray-700">Sort by:</label>
<select
name="ordering"
id="ordering"
onchange="this.form.submit()"
class="block px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
{% for option in sort_options|default:"name,Name (A-Z);-name,Name (Z-A);created_at,Newest First;-created_at,Oldest First" %}
{% with option_parts=option|split:"," %}
<option
value="{{ option_parts.0 }}"
{% if request.GET.ordering == option_parts.0 %}selected{% endif %}
>
{{ option_parts.1 }}
</option>
{% endwith %}
{% endfor %}
</select>
</div>
{% endif %}
</c-slot>
</div>
</form>

View File

@@ -1,39 +0,0 @@
{% comment %}
Cotton Status Badge Component
Converts existing status badge component to use Django Cotton's component system
Preserves canonical status mapping from park_tags
{% endcomment %}
{% load park_tags %}
<!-- Cotton Status Badge Component -->
<c-vars
status="status|default:'UNKNOWN'"
badge_classes="badge_classes|default:''"
size="size|default:'default'"
/>
{% with status_config=status|get_status_config %}
<span class="
inline-flex items-center rounded-full font-medium
{% if size == 'sm' %}
px-2 py-1 text-xs
{% elif size == 'lg' %}
px-3 py-2 text-base
{% else %}
px-2.5 py-0.5 text-sm
{% endif %}
{{ status_config.classes }} {{ badge_classes }}
">
{% if status_config.icon %}
<svg class="-ml-0.5 mr-1.5 h-2 w-2" fill="currentColor" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3" />
</svg>
{% endif %}
<!-- Status Text Content -->
<c-slot name="status-text">
{{ status_config.label }}
</c-slot>
</span>
{% endwith %}

View File

@@ -1,76 +0,0 @@
{% comment %}
Cotton Toast Container Component
Converts the existing toast container to use Django Cotton's component system
{% endcomment %}
{% load static %}
<!-- Cotton Toast Container Component -->
<c-vars
container_classes="{{ container_classes|default:'fixed top-4 right-4 z-50 space-y-2 max-w-sm' }}"
toast_classes="{{ toast_classes|default:'p-4 rounded-lg shadow-lg border backdrop-blur-sm transition-all duration-300 flex items-start gap-3' }}"
icon_classes="{{ icon_classes|default:'w-5 h-5 flex-shrink-0 mt-0.5' }}"
content_classes="{{ content_classes|default:'flex-grow min-w-0' }}"
close_classes="{{ close_classes|default:'p-1 hover:bg-black/10 rounded transition-colors flex-shrink-0' }}"
progress_classes="{{ progress_classes|default:'absolute bottom-0 left-0 h-1 bg-current opacity-30 transition-all duration-100 ease-linear' }}"
/>
<div
x-data="{}"
x-show="$store.toast.toasts.length > 0"
x-cloak
class="{{ container_classes }}"
>
<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 scale-95 translate-x-full"
x-transition:enter-end="transform opacity-100 scale-100 translate-x-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="transform opacity-100 scale-100 translate-x-0"
x-transition:leave-end="transform opacity-0 scale-95 translate-x-full"
class="{{ toast_classes }}"
:class="{
'bg-green-50 border-green-200 text-green-800': toast.type === 'success',
'bg-red-50 border-red-200 text-red-800': toast.type === 'error',
'bg-blue-50 border-blue-200 text-blue-800': toast.type === 'info',
'bg-yellow-50 border-yellow-200 text-yellow-800': toast.type === 'warning'
}"
>
<!-- Toast Icon -->
<div class="{{ icon_classes }}">
<i
:class="{
'fas fa-check-circle text-green-600': toast.type === 'success',
'fas fa-exclamation-circle text-red-600': toast.type === 'error',
'fas fa-info-circle text-blue-600': toast.type === 'info',
'fas fa-exclamation-triangle text-yellow-600': toast.type === 'warning'
}"
></i>
</div>
<!-- Toast Content -->
<div class="{{ content_classes }}">
<p class="font-medium text-sm" x-text="toast.message"></p>
<p x-show="toast.description" class="text-xs opacity-90 mt-1" x-text="toast.description"></p>
</div>
<!-- Close Button -->
<button
@click="$store.toast.remove(toast.id)"
class="{{ close_classes }}"
type="button"
>
<i class="fas fa-times w-4 h-4"></i>
</button>
<!-- Progress Bar (if duration is set) -->
<div
x-show="toast.duration"
class="{{ progress_classes }}"
:style="`width: ${toast.progress}%`"
></div>
</div>
</template>
</div>

View File

@@ -69,7 +69,7 @@
{% endif %}
{% if show_map_action %}
<button onclick="showOnMap('{{ location.type }}', '{{ location.id }}')"
<button onclick="showOnMap('{{ location.type }}', {{ location.id }})"
class="px-3 py-2 text-sm text-green-600 border border-green-600 rounded-lg hover:bg-green-50 dark:hover:bg-green-900 transition-colors"
title="Show on map">
<i class="fas fa-map-marker-alt"></i>
@@ -77,7 +77,7 @@
{% endif %}
{% if show_trip_action %}
<button onclick="addToTrip('{{ location.id }}', '{{ location.type }}', '{{ location.name|escapejs }}')"
<button onclick="addToTrip({{ location|safe }})"
class="px-3 py-2 text-sm text-purple-600 border border-purple-600 rounded-lg hover:bg-purple-50 dark:hover:bg-purple-900 transition-colors"
title="Add to trip">
<i class="fas fa-plus"></i>