Files
thrillwiki_django_no_react/plans/frontend-rewrite-plan.md
pacnpal b9063ff4f8 feat: Add detailed park and ride pages with HTMX integration
- Implemented park detail page with dynamic content loading for rides and weather.
- Created park list page with filters and search functionality.
- Developed ride detail page showcasing ride stats, reviews, and similar rides.
- Added ride list page with filtering options and dynamic loading.
- Introduced search results page with tabs for parks, rides, and users.
- Added HTMX tests for global search functionality.
2025-12-19 19:53:20 -05:00

30 KiB

ThrillWiki Frontend Rewrite Plan

HTMX + Alpine.js Enhanced Implementation

Created: 2025-12-20
Status: Planning
Scope: Complete frontend redesign for enhanced design, modularity, and feature parity


Executive Summary

This plan outlines a comprehensive rewrite of the ThrillWiki HTMX/Alpine.js frontend to achieve:

  1. Enhanced Design - Modern UI with Shadcn UI patterns, animations, and polished UX
  2. Improved Modularity - Reusable component architecture with clear separation of concerns
  3. Feature Parity - Full functionality matching the previous React frontend

Current State Analysis

Based on the CRITICAL_ANALYSIS_HTMX_ALPINE.md, the existing implementation has:

  • Basic template structure with Tailwind CSS
  • Design tokens and component library documentation
  • Basic HTMX integration for partial page updates
  • Simple Alpine.js state management
  • Dark/light mode support
  • ~60-70% missing functionality vs React frontend
  • Limited component reusability
  • Incomplete state management patterns
  • Missing advanced UI components

Architecture Overview

graph TB
    subgraph Frontend Architecture
        subgraph Templates Layer
            BASE[base/base.html]
            LAYOUTS[Layout Templates]
            PAGES[Page Templates]
            COMPONENTS[Component Templates]
            PARTIALS[HTMX Partials]
        end
        
        subgraph Alpine.js Layer
            STORES[Global Stores]
            COMPONENTS_JS[Component Data]
            UTILS[Utility Functions]
        end
        
        subgraph HTMX Layer
            TRIGGERS[Event Triggers]
            TARGETS[Swap Targets]
            INDICATORS[Loading States]
        end
        
        subgraph Design System
            TOKENS[Design Tokens CSS]
            TAILWIND[Tailwind Config]
            SHADCN[Shadcn UI Components]
        end
    end
    
    BASE --> LAYOUTS
    LAYOUTS --> PAGES
    PAGES --> COMPONENTS
    COMPONENTS --> PARTIALS
    
    STORES --> COMPONENTS_JS
    COMPONENTS_JS --> UTILS
    
    TOKENS --> TAILWIND
    TAILWIND --> SHADCN

Phase 1: Foundation & Infrastructure

1.1 Design System Enhancement

Design Tokens Consolidation

  • Merge existing CSS variables from base.html with design-tokens.md
  • Create unified CSS custom properties file: static/css/design-tokens.css
  • Implement CSS layers for better cascade management
/* Proposed CSS Layer Structure */
@layer reset, tokens, base, components, utilities, overrides;

Tailwind CSS 4 Migration

  • Update tailwind.config.js for Tailwind CSS 4 compatibility
  • Extend color palette with design token references
  • Add HTMX state variants (existing htmx-settling, htmx-request, etc.)
  • Configure container queries for component-level responsiveness

Shadcn UI Integration

Create Django template versions of key Shadcn UI components:

  • Button variants (primary, secondary, outline, ghost, destructive)
  • Card component with header, body, footer slots
  • Dialog/Modal with focus trapping
  • Dropdown Menu with keyboard navigation
  • Form components (Input, Select, Checkbox, Radio, Switch)
  • Toast/Notification system
  • Skeleton loading components
  • Avatar with fallback
  • Badge variants
  • Tabs component
  • Accordion/Collapsible
  • Command palette (for advanced search)

1.2 Template Architecture Restructure

Proposed Directory Structure

templates/
├── base/
│   ├── base.html              # Root template with design system
│   └── htmx-base.html         # HTMX-specific base for partials
├── layouts/
│   ├── default.html           # Standard page layout
│   ├── dashboard.html         # User dashboard layout
│   ├── auth.html              # Authentication pages layout
│   ├── sidebar.html           # Sidebar navigation layout
│   └── full-width.html        # Full-width content layout
├── components/
│   ├── ui/                    # Shadcn-style UI components
│   │   ├── button.html
│   │   ├── card.html
│   │   ├── dialog.html
│   │   ├── dropdown.html
│   │   ├── form/
│   │   │   ├── input.html
│   │   │   ├── select.html
│   │   │   ├── checkbox.html
│   │   │   └── textarea.html
│   │   ├── toast.html
│   │   ├── skeleton.html
│   │   └── avatar.html
│   ├── navigation/
│   │   ├── navbar.html
│   │   ├── mobile-menu.html
│   │   ├── breadcrumbs.html
│   │   ├── pagination.html
│   │   └── user-menu.html
│   ├── search/
│   │   ├── search-bar.html
│   │   ├── search-results.html
│   │   ├── autocomplete.html
│   │   └── filters.html
│   ├── cards/
│   │   ├── park-card.html
│   │   ├── ride-card.html
│   │   ├── manufacturer-card.html
│   │   └── operator-card.html
│   └── data-display/
│       ├── stats-card.html
│       ├── data-table.html
│       ├── image-gallery.html
│       └── rating-display.html
├── pages/
│   ├── home/
│   │   └── homepage.html
│   ├── parks/
│   │   ├── list.html
│   │   ├── detail.html
│   │   ├── create.html
│   │   └── edit.html
│   ├── rides/
│   │   ├── list.html
│   │   ├── detail.html
│   │   ├── create.html
│   │   └── edit.html
│   ├── auth/
│   │   ├── login.html
│   │   ├── register.html
│   │   ├── forgot-password.html
│   │   └── reset-password.html
│   ├── user/
│   │   ├── profile.html
│   │   ├── settings.html
│   │   └── dashboard.html
│   └── search/
│       └── results.html
├── partials/                   # HTMX swap targets
│   ├── homepage/
│   ├── parks/
│   ├── rides/
│   ├── search/
│   └── user/
└── emails/                     # Email templates

1.3 Alpine.js Architecture

Global Store System

Create centralized Alpine.js stores in static/js/stores/:

// static/js/stores/index.js
document.addEventListener('alpine:init', () => {
    // Authentication Store
    Alpine.store('auth', {
        user: null,
        isAuthenticated: false,
        permissions: [],
        
        init() {
            // Initialize from server-rendered data
            this.user = window.__AUTH_USER__ || null;
            this.isAuthenticated = !!this.user;
        },
        
        async login(credentials) { /* ... */ },
        async logout() { /* ... */ },
        hasPermission(permission) { /* ... */ }
    });
    
    // Theme Store
    Alpine.store('theme', {
        isDark: false,
        
        init() {
            this.isDark = localStorage.getItem('theme') === 'dark' ||
                (!localStorage.getItem('theme') && 
                 window.matchMedia('(prefers-color-scheme: dark)').matches);
            this.apply();
        },
        
        toggle() {
            this.isDark = !this.isDark;
            this.apply();
        },
        
        apply() {
            document.documentElement.classList.toggle('dark', this.isDark);
            localStorage.setItem('theme', this.isDark ? 'dark' : 'light');
        }
    });
    
    // Search Store
    Alpine.store('search', {
        query: '',
        results: [],
        isOpen: false,
        isLoading: false,
        filters: {},
        
        async search(query) { /* ... */ },
        clearSearch() { /* ... */ },
        applyFilters(filters) { /* ... */ }
    });
    
    // Toast/Notification Store
    Alpine.store('toast', {
        toasts: [],
        
        show(message, type = 'info', duration = 5000) {
            const id = Date.now();
            this.toasts.push({ id, message, type, duration });
            if (duration > 0) {
                setTimeout(() => this.dismiss(id), duration);
            }
            return id;
        },
        
        dismiss(id) {
            this.toasts = this.toasts.filter(t => t.id !== id);
        },
        
        success(message) { return this.show(message, 'success'); },
        error(message) { return this.show(message, 'error'); },
        warning(message) { return this.show(message, 'warning'); },
        info(message) { return this.show(message, 'info'); }
    });
    
    // UI State Store
    Alpine.store('ui', {
        sidebarOpen: false,
        modalStack: [],
        
        openModal(id) { this.modalStack.push(id); },
        closeModal(id) { 
            this.modalStack = this.modalStack.filter(m => m !== id); 
        },
        isModalOpen(id) { return this.modalStack.includes(id); }
    });
});

Reusable Component Functions

Create component-specific Alpine.js data functions:

// static/js/components/index.js

// Modal Component
Alpine.data('modal', (config = {}) => ({
    isOpen: false,
    modalId: config.id || 'default',
    closeOnBackdrop: config.closeOnBackdrop !== false,
    closeOnEscape: config.closeOnEscape !== false,
    
    init() {
        if (this.closeOnEscape) {
            document.addEventListener('keydown', (e) => {
                if (e.key === 'Escape' && this.isOpen) this.close();
            });
        }
    },
    
    open() {
        this.isOpen = true;
        document.body.style.overflow = 'hidden';
        this.$dispatch('modal-opened', { id: this.modalId });
    },
    
    close() {
        this.isOpen = false;
        document.body.style.overflow = '';
        this.$dispatch('modal-closed', { id: this.modalId });
    },
    
    toggle() {
        this.isOpen ? this.close() : this.open();
    }
}));

// Dropdown Component
Alpine.data('dropdown', (config = {}) => ({
    isOpen: false,
    placement: config.placement || 'bottom-start',
    
    toggle() { this.isOpen = !this.isOpen; },
    close() { this.isOpen = false; },
    
    // Keyboard navigation
    handleKeydown(event) {
        if (event.key === 'ArrowDown') { /* ... */ }
        if (event.key === 'ArrowUp') { /* ... */ }
        if (event.key === 'Enter') { /* ... */ }
    }
}));

// Search with Autocomplete
Alpine.data('searchAutocomplete', (config = {}) => ({
    query: '',
    results: [],
    isLoading: false,
    selectedIndex: -1,
    minChars: config.minChars || 2,
    debounceMs: config.debounceMs || 300,
    endpoint: config.endpoint || '/api/v1/search/',
    
    async search() {
        if (this.query.length < this.minChars) {
            this.results = [];
            return;
        }
        
        this.isLoading = true;
        // Debounced search implementation
    },
    
    selectResult(index) { /* ... */ },
    handleKeydown(event) { /* ... */ },
    clear() { /* ... */ }
}));

// Form Validation
Alpine.data('formValidation', (rules = {}) => ({
    errors: {},
    touched: {},
    isSubmitting: false,
    
    validate(field, value) { /* ... */ },
    validateAll() { /* ... */ },
    isValid() { return Object.keys(this.errors).length === 0; },
    hasError(field) { return this.touched[field] && this.errors[field]; },
    reset() { /* ... */ }
}));

// Tabs Component
Alpine.data('tabs', (config = {}) => ({
    activeTab: config.defaultTab || 0,
    
    setActiveTab(index) {
        this.activeTab = index;
        this.$dispatch('tab-changed', { index });
    },
    
    isActive(index) {
        return this.activeTab === index;
    }
}));

// Image Gallery with Lightbox
Alpine.data('imageGallery', () => ({
    images: [],
    currentIndex: 0,
    isLightboxOpen: false,
    
    openLightbox(index) { /* ... */ },
    closeLightbox() { /* ... */ },
    next() { /* ... */ },
    prev() { /* ... */ }
}));

Phase 2: Core Component Implementation

2.1 Navigation Components

Enhanced Navbar

  • Sticky header with backdrop blur
  • Responsive design with mobile drawer
  • User authentication state handling
  • Search integration with command palette (Cmd/Ctrl + K)
  • Theme toggle with system preference detection
  • Active route highlighting

Mobile Navigation

  • Slide-in drawer with gesture support
  • Focus trapping for accessibility
  • Smooth animations with Alpine.js transitions
  • Hierarchical menu support

Breadcrumbs

  • Automatic generation from URL structure
  • Schema.org markup for SEO
  • Mobile-friendly truncation

2.2 Card Components

Park Card

<!-- Template: components/cards/park-card.html -->
{% load static %}

<article class="card card-hover group" 
         x-data="{ isHovered: false }"
         @mouseenter="isHovered = true"
         @mouseleave="isHovered = false">
    
    <!-- Image with overlay -->
    <div class="card-image">
        <img src="{{ park.image_url|default:'/static/images/placeholder-park.jpg' }}" 
             alt="{{ park.name }}"
             class="card-image-img"
             loading="lazy">
        
        <!-- Status Badge -->
        <div class="card-image-overlay">
            <span class="badge badge-{{ park.status }}">
                {{ park.get_status_display }}
            </span>
        </div>
        
        <!-- Hover Actions -->
        <div x-show="isHovered"
             x-transition:enter="transition ease-out duration-200"
             x-transition:enter-start="opacity-0"
             x-transition:enter-end="opacity-100"
             class="absolute inset-0 flex items-end p-4 bg-gradient-to-t from-black/60 to-transparent">
            <div class="flex gap-2">
                <button class="btn btn-sm btn-secondary">
                    {% include 'components/icons/heart.html' %}
                </button>
                <button class="btn btn-sm btn-secondary">
                    {% include 'components/icons/share.html' %}
                </button>
            </div>
        </div>
    </div>
    
    <!-- Content -->
    <div class="card-body">
        <h3 class="card-title">
            <a href="{{ park.get_absolute_url }}" class="card-title-link">
                {{ park.name }}
            </a>
        </h3>
        
        <p class="card-text">{{ park.description|truncatewords:25 }}</p>
        
        <!-- Meta Information -->
        <div class="card-meta">
            <span class="card-meta-item">
                {% include 'components/icons/location.html' with class='icon-xs' %}
                {{ park.location.city }}, {{ park.location.country }}
            </span>
            <span class="card-meta-item">
                {% include 'components/icons/ride.html' with class='icon-xs' %}
                {{ park.rides_count }} rides
            </span>
        </div>
        
        <!-- Rating -->
        {% if park.average_rating %}
        <div class="flex items-center gap-2 mt-3">
            {% include 'components/data-display/rating-stars.html' with rating=park.average_rating %}
            <span class="text-sm text-muted-foreground">
                ({{ park.review_count }} reviews)
            </span>
        </div>
        {% endif %}
    </div>
</article>

Ride Card

  • Similar structure with ride-specific data
  • Category/type badges
  • Height/intensity indicators
  • Manufacturer attribution

2.3 Form Components

Enhanced Input Component

<!-- Template: components/ui/form/input.html -->
{% load widget_tweaks %}

<div class="form-group" 
     x-data="{ focused: false, hasValue: {{ field.value|yesno:'true,false' }} }">
    
    {% if label %}
    <label for="{{ field.id_for_label }}" 
           class="form-label"
           :class="{ 'text-primary': focused }">
        {{ label }}
        {% if field.field.required %}
        <span class="text-error-500" aria-label="required">*</span>
        {% endif %}
    </label>
    {% endif %}
    
    <div class="relative">
        {% if icon_left %}
        <div class="absolute -translate-y-1/2 left-3 top-1/2 text-muted-foreground">
            {{ icon_left }}
        </div>
        {% endif %}
        
        {{ field|add_class:"form-input"|attr:"x-on:focus=focused = true"|attr:"x-on:blur=focused = false; hasValue = $el.value.length > 0" }}
        
        {% if icon_right %}
        <div class="absolute -translate-y-1/2 right-3 top-1/2 text-muted-foreground">
            {{ icon_right }}
        </div>
        {% endif %}
    </div>
    
    {% if field.help_text %}
    <p class="form-help">{{ field.help_text }}</p>
    {% endif %}
    
    {% if field.errors %}
    <div class="form-error" role="alert">
        {{ field.errors.0 }}
    </div>
    {% endif %}
</div>

2.4 Modal/Dialog System

HTMX-Powered Modal

<!-- Template: components/ui/dialog.html -->
<div x-data="modal({ id: '{{ modal_id }}', closeOnBackdrop: {{ close_on_backdrop|default:'true' }} })"
     x-show="isOpen"
     x-on:open-modal.window="if ($event.detail.id === modalId) open()"
     x-on:close-modal.window="if ($event.detail.id === modalId) close()"
     class="modal-overlay"
     x-transition:enter="transition ease-out duration-300"
     x-transition:enter-start="opacity-0"
     x-transition:enter-end="opacity-100"
     x-transition:leave="transition ease-in duration-200"
     x-transition:leave-start="opacity-100"
     x-transition:leave-end="opacity-0"
     role="dialog"
     aria-modal="true"
     aria-labelledby="modal-title-{{ modal_id }}">
    
    <!-- Backdrop -->
    <div class="modal-backdrop" 
         @click="closeOnBackdrop && close()"
         aria-hidden="true"></div>
    
    <!-- Modal Content -->
    <div class="modal-container"
         x-transition:enter="transition ease-out duration-300"
         x-transition:enter-start="opacity-0 scale-95"
         x-transition:enter-end="opacity-100 scale-100"
         x-transition:leave="transition ease-in duration-200"
         x-transition:leave-start="opacity-100 scale-100"
         x-transition:leave-end="opacity-0 scale-95">
        
        <div class="modal-content">
            {% if show_header %}
            <div class="modal-header">
                <h2 id="modal-title-{{ modal_id }}" class="modal-title">
                    {{ title }}
                </h2>
                <button @click="close()" 
                        class="btn-icon"
                        aria-label="Close modal">
                    {% include 'components/icons/x.html' %}
                </button>
            </div>
            {% endif %}
            
            <div class="modal-body" 
                 id="modal-body-{{ modal_id }}"
                 hx-target="this"
                 hx-swap="innerHTML">
                {% block modal_content %}{% endblock %}
            </div>
            
            {% if show_footer %}
            <div class="modal-footer">
                {% block modal_footer %}
                <button @click="close()" class="btn btn-secondary">Cancel</button>
                <button class="btn btn-primary">Confirm</button>
                {% endblock %}
            </div>
            {% endif %}
        </div>
    </div>
</div>

2.5 Toast/Notification System

<!-- Template: components/ui/toast-container.html -->
<div x-data
     class="fixed z-50 flex flex-col max-w-sm gap-2 bottom-4 right-4">
    
    <template x-for="toast in $store.toast.toasts" :key="toast.id">
        <div x-show="true"
             x-transition:enter="transition ease-out duration-300"
             x-transition:enter-start="opacity-0 translate-y-2"
             x-transition:enter-end="opacity-100 translate-y-0"
             x-transition:leave="transition ease-in duration-200"
             x-transition:leave-start="opacity-100"
             x-transition:leave-end="opacity-0 translate-x-full"
             :class="{
                 'bg-success-50 border-success-200 text-success-800': toast.type === 'success',
                 'bg-error-50 border-error-200 text-error-800': toast.type === 'error',
                 'bg-warning-50 border-warning-200 text-warning-800': toast.type === 'warning',
                 'bg-info-50 border-info-200 text-info-800': toast.type === 'info'
             }"
             class="flex items-start gap-3 p-4 border rounded-lg shadow-lg">
            
            <!-- Icon -->
            <div class="flex-shrink-0">
                <template x-if="toast.type === 'success'">
                    {% include 'components/icons/check-circle.html' %}
                </template>
                <template x-if="toast.type === 'error'">
                    {% include 'components/icons/x-circle.html' %}
                </template>
                <!-- ... other icons -->
            </div>
            
            <!-- Content -->
            <div class="flex-1">
                <p x-text="toast.message" class="text-sm font-medium"></p>
            </div>
            
            <!-- Dismiss -->
            <button @click="$store.toast.dismiss(toast.id)"
                    class="flex-shrink-0 p-1 transition-colors rounded hover:bg-black/10">
                {% include 'components/icons/x.html' with class='w-4 h-4' %}
            </button>
        </div>
    </template>
</div>

Phase 3: Feature Implementation

3.1 Authentication System

Modal-Based Authentication

  • Login modal with form validation
  • Registration with multi-step wizard
  • Password reset flow
  • Social authentication integration
  • Remember me functionality
  • Session management with HTMX

User Profile & Settings

  • Profile editing with image upload
  • Account settings management
  • Notification preferences
  • Privacy settings
  • Connected accounts

3.2 Advanced Search System

Global Search with Command Palette

<!-- Cmd/Ctrl + K activated search -->
<div x-data="commandPalette()"
     @keydown.window.cmd.k.prevent="open()"
     @keydown.window.ctrl.k.prevent="open()">
    
    <div x-show="isOpen" 
         class="command-palette-overlay"
         x-transition>
        
        <div class="command-palette">
            <div class="command-palette-input-wrapper">
                <input type="text"
                       x-model="query"
                       @input.debounce.200ms="search()"
                       @keydown.arrow-down.prevent="navigateDown()"
                       @keydown.arrow-up.prevent="navigateUp()"
                       @keydown.enter.prevent="selectCurrent()"
                       @keydown.escape="close()"
                       placeholder="Search parks, rides, users..."
                       class="command-palette-input"
                       x-ref="input">
            </div>
            
            <div class="command-palette-results">
                <!-- Results grouped by category -->
                <template x-for="(group, groupName) in groupedResults">
                    <div class="command-palette-group">
                        <div class="command-palette-group-header" x-text="groupName"></div>
                        <template x-for="(result, index) in group">
                            <a :href="result.url"
                               class="command-palette-item"
                               :class="{ 'active': isSelected(result) }">
                                <!-- Result item -->
                            </a>
                        </template>
                    </div>
                </template>
            </div>
        </div>
    </div>
</div>

Filter System with URL Sync

  • Multi-select filters
  • Range sliders for numeric values
  • Location-based filtering
  • Real-time result updates via HTMX
  • URL state synchronization for shareability

3.3 Data Tables

Advanced Data Table Component

  • Sortable columns
  • Pagination with HTMX
  • Row selection
  • Bulk actions
  • Column visibility toggle
  • Export functionality
  • Responsive design with horizontal scroll
  • Lazy loading with intersection observer
  • Touch gestures for mobile
  • Keyboard navigation
  • Zoom functionality
  • Image upload with drag & drop
  • Progress indicators

Phase 4: HTMX Integration Patterns

4.1 Standard Patterns

Partial Updates

<!-- Trigger partial update -->
<button hx-get="/api/v1/parks/{{ park.id }}/details/"
        hx-target="#park-details"
        hx-swap="innerHTML"
        hx-indicator="#loading-spinner"
        class="btn btn-primary">
    Load Details
</button>

<div id="park-details">
    <!-- Content loaded here -->
</div>

<div id="loading-spinner" class="htmx-indicator">
    {% include 'components/ui/skeleton.html' %}
</div>

Infinite Scroll

<div id="park-list" 
     hx-get="/api/v1/parks/?page={{ next_page }}"
     hx-trigger="revealed"
     hx-swap="beforeend"
     hx-indicator="#infinite-loader">
    
    {% for park in parks %}
        {% include 'components/cards/park-card.html' %}
    {% endfor %}
</div>

<div id="infinite-loader" class="py-8 text-center htmx-indicator">
    <div class="inline-block w-8 h-8 border-2 rounded-full animate-spin border-primary border-t-transparent"></div>
</div>

Optimistic Updates

<button hx-post="/api/v1/parks/{{ park.id }}/favorite/"
        hx-swap="outerHTML"
        hx-on::before-request="this.classList.add('is-favorited')"
        hx-on::response-error="this.classList.remove('is-favorited')"
        class="btn-icon favorite-btn">
    {% include 'components/icons/heart.html' %}
</button>

4.2 Error Handling

// Global HTMX error handling
document.addEventListener('htmx:responseError', (event) => {
    const status = event.detail.xhr.status;
    
    if (status === 401) {
        Alpine.store('toast').error('Please log in to continue');
        // Optionally open login modal
    } else if (status === 403) {
        Alpine.store('toast').error('You do not have permission to perform this action');
    } else if (status === 422) {
        // Validation errors - handled by individual forms
    } else if (status >= 500) {
        Alpine.store('toast').error('Something went wrong. Please try again.');
    }
});

// Loading state management
document.addEventListener('htmx:beforeRequest', (event) => {
    event.target.classList.add('is-loading');
});

document.addEventListener('htmx:afterRequest', (event) => {
    event.target.classList.remove('is-loading');
});

4.3 HX-Trigger Events

# Backend response with HX-Trigger
from django.http import HttpResponse

def favorite_park(request, pk):
    # ... logic
    response = HttpResponse(render_to_string('partials/favorite-button.html', {...}))
    response['HX-Trigger'] = json.dumps({
        'showToast': {'message': 'Park added to favorites', 'type': 'success'},
        'updateFavoritesCount': {'count': new_count}
    })
    return response
// Listen for custom HX-Trigger events
document.addEventListener('showToast', (event) => {
    const { message, type } = event.detail;
    Alpine.store('toast').show(message, type);
});

document.addEventListener('updateFavoritesCount', (event) => {
    const { count } = event.detail;
    document.querySelector('#favorites-count').textContent = count;
});

Phase 5: Performance & Optimization

5.1 Asset Optimization

  • CSS minification and purging
  • JavaScript bundling with proper chunking
  • Image optimization with WebP/AVIF support
  • Lazy loading for below-fold content
  • Preloading critical assets

5.2 Caching Strategy

<!-- Cache partial responses -->
<div hx-get="/api/v1/parks/featured/"
     hx-trigger="load"
     hx-swap="innerHTML"
     hx-headers='{"X-Cache-Control": "max-age=300"}'>
</div>

5.3 Performance Monitoring

  • Core Web Vitals tracking
  • HTMX request timing
  • Alpine.js component performance
  • Error tracking and reporting

Phase 6: Testing Strategy

6.1 Unit Testing

  • Alpine.js store functions
  • Utility JavaScript functions
  • Django template tags

6.2 Integration Testing

  • HTMX endpoint responses
  • Component rendering
  • Form submissions
  • Authentication flows

6.3 E2E Testing

  • User journeys with Playwright
  • Visual regression testing
  • Accessibility testing (axe-core)
  • Cross-browser testing

6.4 Accessibility Testing

  • WCAG 2.1 AA compliance
  • Screen reader compatibility
  • Keyboard navigation
  • Color contrast verification
  • Focus management

Implementation Phases & Milestones

Phase 1: Foundation (Week 1-2)

  • Design system CSS consolidation
  • Tailwind CSS 4 configuration
  • Template directory restructure
  • Alpine.js store architecture
  • Base component implementation

Phase 2: Core Components (Week 3-4)

  • Navigation components
  • Card components (park, ride, etc.)
  • Form components
  • Modal/Dialog system
  • Toast notifications

Phase 3: Feature Pages (Week 5-6)

  • Homepage redesign
  • Park list/detail pages
  • Ride list/detail pages
  • Search results page
  • User profile/settings

Phase 4: Advanced Features (Week 7-8)

  • Authentication system
  • Advanced search/filters
  • Data tables
  • Image gallery
  • Real-time features

Phase 5: Polish & Testing (Week 9-10)

  • Performance optimization
  • Accessibility audit
  • Cross-browser testing
  • Documentation
  • Bug fixes

File Dependencies & Migration Order

graph TD
    A[Design Tokens CSS] --> B[Tailwind Config]
    B --> C[Base Template]
    C --> D[Layout Templates]
    D --> E[UI Components]
    E --> F[Page Templates]
    F --> G[Partials]
    
    H[Alpine.js Stores] --> I[Component Data Functions]
    I --> J[Page-specific JS]
    
    K[HTMX Config] --> L[Endpoint Integration]
    L --> M[Error Handling]

Risk Mitigation

Risk Mitigation Strategy
Breaking existing functionality Implement feature flags, incremental rollout
Performance regression Continuous performance monitoring, benchmarking
Accessibility issues Automated testing with axe-core, manual audits
Browser compatibility Cross-browser testing matrix, progressive enhancement
State management complexity Clear documentation, consistent patterns

Success Metrics

  • Performance: LCP < 2.5s, FID < 100ms, CLS < 0.1
  • Accessibility: WCAG 2.1 AA compliance
  • Code Quality: 80%+ test coverage, no critical linting errors
  • Developer Experience: Component reuse > 70%, clear documentation
  • User Experience: Consistent interactions, smooth animations

Next Steps

  1. Review and approve this plan
  2. Set up development environment with new structure
  3. Begin Phase 1 implementation
  4. Schedule weekly progress reviews

This plan is a living document and will be updated as implementation progresses.