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,4 +1,4 @@
modules = ["bash", "web", "nodejs-20", "python-3.13", "python3"]
modules = ["bash", "web", "nodejs-20", "python-3.13"]
[nix]
channel = "stable-25_05"
@@ -39,10 +39,6 @@ externalPort = 80
localPort = 34277
externalPort = 3000
[[ports]]
localPort = 38955
externalPort = 3001
[deployment]
deploymentTarget = "autoscale"
run = ["gunicorn", "--bind=0.0.0.0:5000", "--reuse-port", "thrillwiki.wsgi:application"]

View File

@@ -1,279 +0,0 @@
# ThrillWiki Django Cotton Conversion Plan
## Overview
This document outlines the comprehensive plan to convert ThrillWiki's entire template system from Django's `{% include %}` pattern to Django Cotton's modern component architecture. This conversion will improve maintainability, reusability, and developer experience while preserving all existing functionality.
## Current State Analysis
### Template Inventory
- **Total Templates**: 147 HTML files
- **Components with {% include %}**: 52+ templates
- **Base UI Components**: 6 components
- **Feature Components**: 40+ domain-specific partials
- **Auth Components**: 4 authentication components
- **Missing Referenced Templates**: 3 card content templates
### Component Categories
#### 1. Foundation UI Components (6)
- `components/ui/button.html` - Reusable button with variants
- `components/ui/card.html` - Standard card layout
- `components/ui/input.html` - Form input component
- `components/pagination.html` - List pagination
- `components/search_form.html` - Search functionality
- `components/status_badge.html` - Status indicators
#### 2. Authentication Components (4)
- `account/partials/login_form.html` - Login form
- `account/partials/signup_modal.html` - Registration modal
- `accounts/turnstile_widget.html` - CAPTCHA widget
- `accounts/turnstile_widget_empty.html` - Empty CAPTCHA state
#### 3. Layout Components (1)
- `components/layout/enhanced_header.html` - Main navigation header
#### 4. Feature Components (18 Rides + 5 Parks + 3 Media + 7 Maps)
- **Rides**: Forms, modals, lists, search results, history panels
- **Parks**: Location widgets, actions, lists, search results
- **Media**: Photo display, upload, management
- **Maps**: Location cards, filter panels, containers, popups
#### 5. Advanced Systems (13 Moderation + 4 Search)
- **Moderation**: Submission workflows, photo management, filtering
- **Search**: Results, filters, location-based search
## Conversion Strategy: 4-Phase Approach
### Phase 1: Foundation UI & Auth Components
**Priority**: CRITICAL | **Duration**: 2-3 days
**Goal**: Convert the most frequently used base components that are referenced throughout the application.
**Components**:
1. UI Components (6): button, card, input, pagination, search_form, status_badge
2. Auth Components (4): login_form, signup_modal, turnstile widgets
**Benefits**:
- Immediate impact across entire application
- Establishes Cotton patterns for team
- Reduces template complexity in header and forms
### Phase 2: Layout & Navigation
**Priority**: HIGH | **Duration**: 3-4 days
**Goal**: Convert major structural components that define application layout.
**Components**:
1. Enhanced header with navigation, search, user menu
2. Filter sidebar with advanced filtering capabilities
**Benefits**:
- Cleaner main layout structure
- Easier header customization
- Modular navigation system
### Phase 3: Feature-Specific Components
**Priority**: MEDIUM | **Duration**: 5-7 days
**Goal**: Convert domain-specific components for core functionality.
**Components**:
1. **Rides Domain** (18 components): Forms, modals, search, management
2. **Parks Domain** (5 components): Location widgets, actions, lists
3. **Media Components** (3 components): Photo handling
4. **Maps Components** (7 components): Location cards, filtering, display
**Benefits**:
- Improved component reusability
- Better separation of concerns
- Easier feature development
### Phase 4: Advanced & Specialized Systems
**Priority**: LOW-MEDIUM | **Duration**: 4-5 days
**Goal**: Convert complex systems with heavy HTMX/Alpine.js integration.
**Components**:
1. **Moderation System** (13 components): Complex workflows
2. **Search System** (4 components): Advanced search features
**Benefits**:
- Complete Cotton migration
- Improved moderation workflows
- Enhanced search capabilities
## Cotton Directory Structure
```
backend/templates/cotton/
├── ui/ # Base UI components
│ ├── button.html
│ ├── card.html
│ ├── input.html
│ ├── pagination.html
│ ├── search_form.html
│ └── status_badge.html
├── auth/ # Authentication components
│ ├── modal.html # ✅ Already converted
│ ├── login_form.html
│ ├── signup_modal.html
│ ├── turnstile_widget.html
│ └── turnstile_empty.html
├── layout/ # Layout components
│ └── header.html
├── features/ # Cross-cutting features
│ └── filter_sidebar.html
├── rides/ # Ride domain components
│ ├── form.html
│ ├── add_modal.html
│ ├── list_results.html
│ └── [15 more components]
├── parks/ # Park domain components
│ ├── location_widget.html
│ ├── actions.html
│ └── [3 more components]
├── maps/ # Map system components
│ ├── location_card.html
│ ├── filter_panel.html
│ └── [5 more components]
├── media/ # Media handling components
│ ├── photo_display.html
│ ├── photo_upload.html
│ └── photo_manager.html
├── moderation/ # Moderation system components
│ └── [13 components]
└── search/ # Search system components
└── [4 components]
```
## Cotton Component Standards
### 1. c-vars Configuration
```django
<c-vars
container_classes="{{ container_classes|default:'default-container-styles' }}"
button_variant="{{ button_variant|default:'primary' }}"
show_actions="{{ show_actions|default:True }}"
/>
```
### 2. c-slot Usage
```django
<c-slot name="header-content">
<!-- Custom header content -->
</c-slot>
<c-slot name="actions">
<!-- Custom action buttons -->
</c-slot>
```
### 3. Alpine.js Preservation
- Maintain all `x-data`, `x-show`, `x-model` directives
- Preserve event handlers (`@click`, `@submit`)
- Keep transitions and animations
- Test JavaScript functionality after conversion
### 4. HTMX Integration
- Preserve all `hx-*` attributes
- Maintain target and swap configurations
- Ensure form submissions work correctly
- Test real-time updates and live search
## Implementation Guidelines
### Conversion Process
1. **Analyze Original Component**: Understand functionality and dependencies
2. **Create Cotton Version**: Convert to Cotton format with c-vars and c-slots
3. **Test in Isolation**: Verify component renders correctly
4. **Update Templates**: Replace include statements with Cotton tags
5. **Integration Testing**: Test with Alpine.js and HTMX
6. **Visual Verification**: Ensure styling and behavior match
### Testing Strategy
1. **Component Testing**: Test each component individually
2. **Integration Testing**: Verify interactions between components
3. **Functionality Testing**: Ensure HTMX/Alpine.js still work
4. **Visual Testing**: Compare before/after screenshots
5. **Performance Testing**: Monitor render times and optimization
### Quality Standards
- **No Breaking Changes**: All existing functionality preserved
- **Improved Performance**: Cotton optimizations applied
- **Better Maintainability**: Cleaner component structure
- **Enhanced Customization**: Flexible styling via c-vars
- **Documentation**: Clear component usage examples
## Success Criteria
### Technical Goals
- [ ] All 62+ components converted to Cotton format
- [ ] Zero template render errors
- [ ] All Alpine.js functionality preserved
- [ ] All HTMX interactions working
- [ ] Responsive design maintained
- [ ] Performance maintained or improved
### Quality Goals
- [ ] Components properly organized in logical directory structure
- [ ] Meaningful c-vars for customization
- [ ] Clear component documentation
- [ ] Consistent naming conventions
- [ ] Reusable component patterns established
## Benefits of Conversion
### Developer Experience
- **Cleaner Templates**: `<c-rides.form />` vs `{% include 'rides/partials/ride_form.html' %}`
- **Better Organization**: Logical component hierarchy
- **Easier Maintenance**: Components in dedicated Cotton directory
- **Type Safety**: Cotton's validation helps catch template errors
### Performance
- **Better Caching**: Cotton optimizes component rendering
- **Reduced Complexity**: Simpler template inheritance chains
- **Faster Development**: Reusable components speed up feature development
### Customization
- **Flexible Styling**: c-vars allow easy theme customization
- **Component Variants**: Different button styles, card layouts, etc.
- **Conditional Rendering**: Better control over component behavior
## Risk Mitigation
### Migration Safety
- **Gradual Migration**: Convert components incrementally
- **Parallel Existence**: Keep old includes until Cotton versions tested
- **Rollback Plan**: Easy to revert individual components if issues arise
- **Comprehensive Testing**: Each phase thoroughly tested before proceeding
### Potential Issues
- **Alpine.js Conflicts**: Careful testing of JavaScript interactions
- **HTMX Target Changes**: Verify all HTMX endpoints still work
- **Styling Regressions**: Visual comparison testing
- **Performance Impact**: Monitor rendering performance
## Timeline
- **Week 1**: Phase 1 - Foundation UI & Auth Components
- **Week 2**: Phase 2 - Layout & Navigation Components
- **Week 3-4**: Phase 3 - Feature Components (parallel development)
- **Week 5**: Phase 4 - Advanced Components
- **Week 6**: Testing, optimization, and cleanup
## Completion Status
### Already Completed ✅
- Django Cotton package installation and configuration
- `cotton/auth/modal.html` - Authentication modal
- `cotton/ui/toast.html` - Toast notifications
- Base template integration (`{% load cotton %}`)
### Remaining Work
- 60+ components to convert
- Template updates across application
- Comprehensive testing and validation
---
This plan provides a structured approach to completely modernize ThrillWiki's template architecture while maintaining all existing functionality and improving developer experience.

View File

@@ -96,7 +96,6 @@ THIRD_PARTY_APPS = [
"django_celery_beat", # Celery beat scheduler
"django_celery_results", # Celery result backend
"django_extensions", # Django Extensions for enhanced development tools
"django_cotton", # Django Cotton for component-based templates
]
LOCAL_APPS = [

View File

@@ -62,7 +62,6 @@ dependencies = [
"djangorestframework-simplejwt>=5.5.1",
"django-forwardemail>=1.0.0",
"django-cloudflareimages-toolkit>=1.0.6",
"django-cotton>=2.1.3",
]
[dependency-groups]

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>

View File

@@ -71,21 +71,6 @@ urlpatterns = [
TemplateView.as_view(template_name="pages/privacy.html"),
name="privacy",
),
path(
"cotton-test/",
TemplateView.as_view(template_name="pages/cotton_test.html"),
name="cotton_test",
),
path(
"cotton-simple/",
TemplateView.as_view(template_name="pages/cotton_simple_test.html"),
name="cotton_simple_test",
),
path(
"cotton-minimal/",
TemplateView.as_view(template_name="pages/cotton_minimal_test.html"),
name="cotton_minimal_test",
),
# Custom authentication URLs first (to override allauth defaults)
path("accounts/", include("apps.accounts.urls")),
# Default allauth URLs (for social auth and other features)

16
backend/uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.13"
[[package]]
@@ -642,18 +642,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/d8/19ed1e47badf477d17fb177c1c19b5a21da0fd2d9f093f23be3fb86c5fab/django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449", size = 12809, upload-time = "2025-09-18T10:40:50.843Z" },
]
[[package]]
name = "django-cotton"
version = "2.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/99/36e318ebd1ace3fc874541a02e7e4149def8e21aab85aceb7bb01e17607b/django_cotton-2.1.3.tar.gz", hash = "sha256:737f9c088549d7febbf78532856ddf1270799675a4bc9fa191a5db0e195a9c13", size = 23432, upload-time = "2025-06-30T17:31:29.29Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/ec/5e5318af0304962be43e3b912aef024d8ac08c0f9a9dfcc4f0cd55d0e74e/django_cotton-2.1.3-py3-none-any.whl", hash = "sha256:f33658d05a8f5ecf7448bdf1089e2ad27d2ce42e59c752216129701d7d153c89", size = 22214, upload-time = "2025-06-30T17:31:28.093Z" },
]
[[package]]
name = "django-debug-toolbar"
version = "6.0.0"
@@ -2250,7 +2238,6 @@ dependencies = [
{ name = "django-cleanup" },
{ name = "django-cloudflareimages-toolkit" },
{ name = "django-cors-headers" },
{ name = "django-cotton" },
{ name = "django-debug-toolbar" },
{ name = "django-environ" },
{ name = "django-extensions" },
@@ -2322,7 +2309,6 @@ requires-dist = [
{ name = "django-cleanup", specifier = ">=8.0.0" },
{ name = "django-cloudflareimages-toolkit", specifier = ">=1.0.6" },
{ name = "django-cors-headers", specifier = ">=4.3.1" },
{ name = "django-cotton", specifier = ">=2.1.3" },
{ name = "django-debug-toolbar", specifier = ">=4.0.0" },
{ name = "django-environ", specifier = ">=0.12.0" },
{ name = "django-extensions", specifier = ">=4.1" },

View File

@@ -21,7 +21,6 @@ ThrillWiki is a comprehensive Django-based web application for theme park and ri
- **Database**: PostgreSQL (DATABASE_URL environment variable)
- **Server**: Django development server on 0.0.0.0:5000
- **Spatial Libraries**: GDAL and GEOS configured with correct Nix store paths
- **Component System**: Django Cotton for modular HTMX frontend components
- **Settings**: Local development configuration active
#### Database Configuration
@@ -36,7 +35,6 @@ Migrations: All applied successfully (including circular dependency resolution)
- GeoDjango with PostGIS support
- Django REST Framework
- Django Allauth (authentication)
- Django Cotton (component system)
- CloudflareImages Toolkit
- Django PGHistory
- Pillow (image processing)
@@ -67,19 +65,6 @@ Migrations: All applied successfully (including circular dependency resolution)
### Recent Setup Work (September 2025)
#### Django Cotton Component System Integration - PHASE 1 COMPLETE ✅
1. **Package Installation**: Added django-cotton>=2.1.3 to dependencies via UV package manager
2. **Configuration**: Integrated Cotton into Django settings via THIRD_PARTY_APPS
3. **Foundation Components Converted (9 total)**:
- **UI Components (6)**: button, card, input, pagination, search_form, status_badge → cotton/ui/
- **Auth Components (3)**: login_form, turnstile_widget, turnstile_empty → cotton/auth/
4. **Template Integration**: Enhanced header updated to use Cotton button components
5. **Cotton Syntax**: All components use proper c-vars without moustaches, string boolean handling
6. **Integration Preserved**: HTMX and Alpine.js functionality maintained throughout
7. **Quality Standards**: Accessibility, security, and canonical mappings preserved
8. **Result**: Production-ready Cotton foundation with 9 converted components, architect-approved
#### Database Migration Strategy
1. **Initial Issue**: Circular dependency between `accounts.0001_initial` and `django_cloudflareimages_toolkit.0001_initial`
2. **Solution**: Split migrations into stages: