mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 15:11:09 -05:00
Restored to 'ba32d51b3eb6866667ec8382daca17202cf7da86'
Replit-Restored-To: ba32d51b3eb6866667ec8382daca17202cf7da86
This commit is contained in:
6
.replit
6
.replit
@@ -1,4 +1,4 @@
|
|||||||
modules = ["bash", "web", "nodejs-20", "python-3.13", "python3"]
|
modules = ["bash", "web", "nodejs-20", "python-3.13"]
|
||||||
|
|
||||||
[nix]
|
[nix]
|
||||||
channel = "stable-25_05"
|
channel = "stable-25_05"
|
||||||
@@ -39,10 +39,6 @@ externalPort = 80
|
|||||||
localPort = 34277
|
localPort = 34277
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 38955
|
|
||||||
externalPort = 3001
|
|
||||||
|
|
||||||
[deployment]
|
[deployment]
|
||||||
deploymentTarget = "autoscale"
|
deploymentTarget = "autoscale"
|
||||||
run = ["gunicorn", "--bind=0.0.0.0:5000", "--reuse-port", "thrillwiki.wsgi:application"]
|
run = ["gunicorn", "--bind=0.0.0.0:5000", "--reuse-port", "thrillwiki.wsgi:application"]
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -96,7 +96,6 @@ THIRD_PARTY_APPS = [
|
|||||||
"django_celery_beat", # Celery beat scheduler
|
"django_celery_beat", # Celery beat scheduler
|
||||||
"django_celery_results", # Celery result backend
|
"django_celery_results", # Celery result backend
|
||||||
"django_extensions", # Django Extensions for enhanced development tools
|
"django_extensions", # Django Extensions for enhanced development tools
|
||||||
"django_cotton", # Django Cotton for component-based templates
|
|
||||||
]
|
]
|
||||||
|
|
||||||
LOCAL_APPS = [
|
LOCAL_APPS = [
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ dependencies = [
|
|||||||
"djangorestframework-simplejwt>=5.5.1",
|
"djangorestframework-simplejwt>=5.5.1",
|
||||||
"django-forwardemail>=1.0.0",
|
"django-forwardemail>=1.0.0",
|
||||||
"django-cloudflareimages-toolkit>=1.0.6",
|
"django-cloudflareimages-toolkit>=1.0.6",
|
||||||
"django-cotton>=2.1.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load cotton %}
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -128,11 +127,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Global Auth Modal (Cotton Component) -->
|
<!-- Global Auth Modal -->
|
||||||
<c-auth.modal />
|
{% include 'components/auth/auth-modal.html' %}
|
||||||
|
|
||||||
<!-- Global Toast Container (Cotton Component) -->
|
<!-- Global Toast Container -->
|
||||||
<c-ui.toast />
|
{% include 'components/ui/toast-container.html' %}
|
||||||
|
|
||||||
<!-- Custom JavaScript with cache control -->
|
<!-- Custom JavaScript with cache control -->
|
||||||
<script src="{% static 'js/main.js' %}?v={{ version|default:'1.0' }}"></script>
|
<script src="{% static 'js/main.js' %}?v={{ version|default:'1.0' }}"></script>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
|
|||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load static %}
|
{% 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">
|
<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">
|
<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"
|
hx-include="this"
|
||||||
name="q"
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Search Results Dropdown -->
|
<!-- Search Results Dropdown -->
|
||||||
@@ -238,7 +237,7 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="hidden md:flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
@click="window.authModal.show('login')"
|
@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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="hidden md:flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<div
|
<div
|
||||||
hx-get="{% url 'account_login' %}"
|
hx-get="{% url 'account_login' %}"
|
||||||
hx-target="body"
|
hx-target="body"
|
||||||
hx-swap="beforeend"
|
hx-swap="beforeend"
|
||||||
class="cursor-pointer"
|
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>
|
||||||
<div
|
<div
|
||||||
hx-get="{% url 'account_signup' %}"
|
hx-get="{% url 'account_signup' %}"
|
||||||
@@ -324,13 +323,13 @@ Includes: Browse menu, advanced search, theme toggle, user dropdown, mobile menu
|
|||||||
hx-swap="beforeend"
|
hx-swap="beforeend"
|
||||||
class="cursor-pointer"
|
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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Mobile Menu Button -->
|
<!-- Mobile Menu Button -->
|
||||||
<div class="md:hidden" x-data="{ open: false }">
|
<div x-data="{ open: false }">
|
||||||
<button
|
<button
|
||||||
@click="open = !open"
|
@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"
|
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
|
Navigate through the ultimate theme park database
|
||||||
</p>
|
</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 -->
|
<!-- Navigation Section -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Search Bar -->
|
<!-- Mobile Search Bar -->
|
||||||
<div class="md:hidden border-t border-border bg-background">
|
<div class="md:hidden border-t bg-background">
|
||||||
<div class="px-4 py-4">
|
<div class="px-4 py-3">
|
||||||
<div class="bg-accent/30 rounded-lg p-3 border border-border">
|
<div class="relative">
|
||||||
<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>
|
<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
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search parks, rides..."
|
placeholder="Search parks, rides..."
|
||||||
input_classes="pl-10 flex-1"
|
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-get="{% url 'search:search' %}"
|
||||||
hx_trigger="input changed delay:300ms"
|
hx-trigger="input changed delay:300ms"
|
||||||
hx_target="#mobile-search-results"
|
hx-target="#mobile-search-results"
|
||||||
hx_include="this"
|
hx-include="this"
|
||||||
name="q"
|
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>
|
||||||
<c-ui.button
|
<div id="mobile-search-results" class="mt-2"></div>
|
||||||
variant="default"
|
|
||||||
size="default"
|
|
||||||
icon_left="fas fa-search"
|
|
||||||
button_classes="px-3 flex-shrink-0"
|
|
||||||
type="submit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="mobile-search-results" class="mt-3"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 %}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 }}
|
|
||||||
/>
|
|
||||||
@@ -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 %}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 %}
|
|
||||||
@@ -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>
|
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_map_action %}
|
{% 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"
|
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">
|
title="Show on map">
|
||||||
<i class="fas fa-map-marker-alt"></i>
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_trip_action %}
|
{% 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"
|
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">
|
title="Add to trip">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
|
|||||||
@@ -71,21 +71,6 @@ urlpatterns = [
|
|||||||
TemplateView.as_view(template_name="pages/privacy.html"),
|
TemplateView.as_view(template_name="pages/privacy.html"),
|
||||||
name="privacy",
|
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)
|
# Custom authentication URLs first (to override allauth defaults)
|
||||||
path("accounts/", include("apps.accounts.urls")),
|
path("accounts/", include("apps.accounts.urls")),
|
||||||
# Default allauth URLs (for social auth and other features)
|
# Default allauth URLs (for social auth and other features)
|
||||||
|
|||||||
16
backend/uv.lock
generated
16
backend/uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "django-debug-toolbar"
|
name = "django-debug-toolbar"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -2250,7 +2238,6 @@ dependencies = [
|
|||||||
{ name = "django-cleanup" },
|
{ name = "django-cleanup" },
|
||||||
{ name = "django-cloudflareimages-toolkit" },
|
{ name = "django-cloudflareimages-toolkit" },
|
||||||
{ name = "django-cors-headers" },
|
{ name = "django-cors-headers" },
|
||||||
{ name = "django-cotton" },
|
|
||||||
{ name = "django-debug-toolbar" },
|
{ name = "django-debug-toolbar" },
|
||||||
{ name = "django-environ" },
|
{ name = "django-environ" },
|
||||||
{ name = "django-extensions" },
|
{ name = "django-extensions" },
|
||||||
@@ -2322,7 +2309,6 @@ requires-dist = [
|
|||||||
{ name = "django-cleanup", specifier = ">=8.0.0" },
|
{ name = "django-cleanup", specifier = ">=8.0.0" },
|
||||||
{ name = "django-cloudflareimages-toolkit", specifier = ">=1.0.6" },
|
{ name = "django-cloudflareimages-toolkit", specifier = ">=1.0.6" },
|
||||||
{ name = "django-cors-headers", specifier = ">=4.3.1" },
|
{ 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-debug-toolbar", specifier = ">=4.0.0" },
|
||||||
{ name = "django-environ", specifier = ">=0.12.0" },
|
{ name = "django-environ", specifier = ">=0.12.0" },
|
||||||
{ name = "django-extensions", specifier = ">=4.1" },
|
{ name = "django-extensions", specifier = ">=4.1" },
|
||||||
|
|||||||
15
replit.md
15
replit.md
@@ -21,7 +21,6 @@ ThrillWiki is a comprehensive Django-based web application for theme park and ri
|
|||||||
- **Database**: PostgreSQL (DATABASE_URL environment variable)
|
- **Database**: PostgreSQL (DATABASE_URL environment variable)
|
||||||
- **Server**: Django development server on 0.0.0.0:5000
|
- **Server**: Django development server on 0.0.0.0:5000
|
||||||
- **Spatial Libraries**: GDAL and GEOS configured with correct Nix store paths
|
- **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
|
- **Settings**: Local development configuration active
|
||||||
|
|
||||||
#### Database Configuration
|
#### Database Configuration
|
||||||
@@ -36,7 +35,6 @@ Migrations: All applied successfully (including circular dependency resolution)
|
|||||||
- GeoDjango with PostGIS support
|
- GeoDjango with PostGIS support
|
||||||
- Django REST Framework
|
- Django REST Framework
|
||||||
- Django Allauth (authentication)
|
- Django Allauth (authentication)
|
||||||
- Django Cotton (component system)
|
|
||||||
- CloudflareImages Toolkit
|
- CloudflareImages Toolkit
|
||||||
- Django PGHistory
|
- Django PGHistory
|
||||||
- Pillow (image processing)
|
- Pillow (image processing)
|
||||||
@@ -67,19 +65,6 @@ Migrations: All applied successfully (including circular dependency resolution)
|
|||||||
|
|
||||||
### Recent Setup Work (September 2025)
|
### 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
|
#### Database Migration Strategy
|
||||||
1. **Initial Issue**: Circular dependency between `accounts.0001_initial` and `django_cloudflareimages_toolkit.0001_initial`
|
1. **Initial Issue**: Circular dependency between `accounts.0001_initial` and `django_cloudflareimages_toolkit.0001_initial`
|
||||||
2. **Solution**: Split migrations into stages:
|
2. **Solution**: Split migrations into stages:
|
||||||
|
|||||||
Reference in New Issue
Block a user