# Django Template & Frontend Architecture Analysis

**Date:** January 7, 2025  
**Analyst:** Roo (Architect Mode)  
**Purpose:** Django template system and frontend architecture analysis for Symfony conversion  
**Status:** Complete frontend layer analysis for conversion planning

## Overview

This document analyzes the Django template system, static asset management, HTMX integration, and frontend architecture to facilitate conversion to Symfony's Twig templating system and modern frontend tooling.

## Template System Architecture

### Django Template Structure
```
templates/
├── base/
│   ├── base.html                    # Main layout
│   ├── header.html                  # Site header
│   ├── footer.html                  # Site footer
│   └── navigation.html              # Main navigation
├── account/
│   ├── login.html                   # Authentication
│   ├── signup.html
│   └── partials/
│       ├── login_form.html          # HTMX login modal
│       └── signup_form.html         # HTMX signup modal
├── parks/
│   ├── list.html                    # Park listing
│   ├── detail.html                  # Park detail page
│   ├── form.html                    # Park edit form
│   └── partials/
│       ├── park_card.html           # HTMX park card
│       ├── park_grid.html           # HTMX park grid
│       ├── rides_section.html       # HTMX rides tab
│       └── photos_section.html      # HTMX photos tab
├── rides/
│   ├── list.html
│   ├── detail.html
│   └── partials/
│       ├── ride_card.html
│       ├── ride_stats.html
│       └── ride_photos.html
├── search/
│   ├── index.html
│   ├── results.html
│   └── partials/
│       ├── suggestions.html         # HTMX autocomplete
│       ├── filters.html             # HTMX filter controls
│       └── results_grid.html        # HTMX results
└── moderation/
    ├── dashboard.html
    ├── submissions.html
    └── partials/
        ├── submission_card.html
        └── approval_form.html
```

### Base Template Analysis

#### Main Layout Template
```html
<!-- templates/base/base.html -->
<!DOCTYPE html>
<html lang="en" data-theme="{{ user.theme_preference|default:'auto' }}">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}ThrillWiki{% endblock %}</title>
    
    <!-- SEO Meta Tags -->
    <meta name="description" content="{% block description %}The ultimate theme park and roller coaster database{% endblock %}">
    <meta name="keywords" content="{% block keywords %}theme parks, roller coasters, rides{% endblock %}">
    
    <!-- Open Graph -->
    <meta property="og:title" content="{% block og_title %}{% block title %}ThrillWiki{% endblock %}{% endblock %}">
    <meta property="og:description" content="{% block og_description %}{% block description %}{% endblock %}{% endblock %}">
    <meta property="og:image" content="{% block og_image %}{% static 'images/og-default.jpg' %}{% endblock %}">
    
    <!-- Tailwind CSS -->
    <link href="{% static 'css/styles.css' %}" rel="stylesheet">
    
    <!-- HTMX -->
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
    
    <!-- Alpine.js -->
    <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
    
    {% block extra_head %}{% endblock %}
</head>
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
    <!-- Navigation -->
    {% include 'base/navigation.html' %}
    
    <!-- Main Content -->
    <main class="container mx-auto px-4 py-8">
        <!-- Messages -->
        {% if messages %}
            <div id="messages" class="mb-4">
                {% for message in messages %}
                    <div class="alert alert-{{ message.tags }} mb-2" 
                         x-data="{ show: true }" 
                         x-show="show" 
                         x-transition>
                        {{ message }}
                        <button @click="show = false" class="ml-2">×</button>
                    </div>
                {% endfor %}
            </div>
        {% endif %}
        
        {% block content %}{% endblock %}
    </main>
    
    <!-- Footer -->
    {% include 'base/footer.html' %}
    
    <!-- HTMX Configuration -->
    <script>
        // HTMX configuration
        htmx.config.defaultSwapStyle = 'innerHTML';
        htmx.config.scrollBehavior = 'smooth';
        
        // CSRF token for HTMX
        document.body.addEventListener('htmx:configRequest', function(evt) {
            evt.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
        });
    </script>
    
    {% block extra_scripts %}{% endblock %}
</body>
</html>
```

#### Navigation Component
```html
<!-- templates/base/navigation.html -->
<nav class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700" 
     x-data="{ mobileOpen: false, userMenuOpen: false }">
    <div class="container mx-auto px-4">
        <div class="flex justify-between items-center py-4">
            <!-- Logo -->
            <a href="{% url 'home' %}" class="text-2xl font-bold text-blue-600 dark:text-blue-400">
                ThrillWiki
            </a>
            
            <!-- Desktop Navigation -->
            <div class="hidden md:flex space-x-8">
                <a href="{% url 'park-list' %}" 
                   class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
                    Parks
                </a>
                <a href="{% url 'ride-list' %}" 
                   class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
                    Rides
                </a>
                <a href="{% url 'search' %}" 
                   class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
                    Search
                </a>
                {% if can_moderate %}
                    <a href="{% url 'moderation-dashboard' %}" 
                       class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
                        Moderation
                        {% if pending_submissions_count > 0 %}
                            <span class="bg-red-500 text-white rounded-full px-2 py-1 text-xs ml-1">
                                {{ pending_submissions_count }}
                            </span>
                        {% endif %}
                    </a>
                {% endif %}
            </div>
            
            <!-- Search Bar -->
            <div class="hidden md:block flex-1 max-w-md mx-8">
                <input type="text" 
                       name="q"
                       placeholder="Search parks, rides..."
                       class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"
                       hx-get="{% url 'search-suggestions' %}"
                       hx-trigger="keyup changed delay:300ms"
                       hx-target="#search-suggestions"
                       hx-indicator="#search-loading">
                <div id="search-suggestions" class="relative"></div>
                <div id="search-loading" class="htmx-indicator">Searching...</div>
            </div>
            
            <!-- User Menu -->
            <div class="relative" x-data="{ open: false }">
                {% if user.is_authenticated %}
                    <button @click="open = !open" 
                            class="flex items-center space-x-2 hover:text-blue-600 dark:hover:text-blue-400">
                        {% if user.userprofile.avatar %}
                            <img src="{{ user.userprofile.avatar.url }}" 
                                 alt="{{ user.username }}" 
                                 class="w-8 h-8 rounded-full">
                        {% else %}
                            <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white">
                                {{ user.username|first|upper }}
                            </div>
                        {% endif %}
                        <span>{{ user.username }}</span>
                    </button>
                    
                    <div x-show="open" 
                         @click.away="open = false"
                         x-transition
                         class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg py-2">
                        <a href="{% url 'profile' user.username %}" 
                           class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700">
                            Profile
                        </a>
                        <a href="{% url 'account_logout' %}" 
                           class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700">
                            Logout
                        </a>
                    </div>
                {% else %}
                    <div class="space-x-4">
                        <button hx-get="{% url 'account_login' %}"
                                hx-target="#auth-modal"
                                hx-swap="innerHTML"
                                class="text-blue-600 dark:text-blue-400 hover:underline">
                            Login
                        </button>
                        <button hx-get="{% url 'account_signup' %}"
                                hx-target="#auth-modal"
                                hx-swap="innerHTML"
                                class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
                            Sign Up
                        </button>
                    </div>
                {% endif %}
            </div>
        </div>
    </div>
    
    <!-- Auth Modal Container -->
    <div id="auth-modal"></div>
</nav>
```

### HTMX Integration Patterns

#### Autocomplete Component
```html
<!-- templates/search/partials/suggestions.html -->
<div class="absolute top-full left-0 right-0 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-b-lg shadow-lg z-50">
    {% if results.parks or results.rides %}
        {% if results.parks %}
            <div class="border-b border-gray-200 dark:border-gray-700">
                <div class="px-4 py-2 text-sm font-semibold text-gray-500 dark:text-gray-400">Parks</div>
                {% for park in results.parks %}
                    <a href="{% url 'park-detail' park.slug %}" 
                       class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 border-b border-gray-100 dark:border-gray-700 last:border-b-0">
                        <div class="font-medium">{{ park.name }}</div>
                        <div class="text-sm text-gray-500 dark:text-gray-400">
                            {{ park.operator.name }} • {{ park.status|title }}
                        </div>
                    </a>
                {% endfor %}
            </div>
        {% endif %}
        
        {% if results.rides %}
            <div>
                <div class="px-4 py-2 text-sm font-semibold text-gray-500 dark:text-gray-400">Rides</div>
                {% for ride in results.rides %}
                    <a href="{% url 'ride-detail' ride.slug %}" 
                       class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 border-b border-gray-100 dark:border-gray-700 last:border-b-0">
                        <div class="font-medium">{{ ride.name }}</div>
                        <div class="text-sm text-gray-500 dark:text-gray-400">
                            {{ ride.park.name }} • {{ ride.get_ride_type_display }}
                        </div>
                    </a>
                {% endfor %}
            </div>
        {% endif %}
    {% else %}
        <div class="px-4 py-2 text-gray-500 dark:text-gray-400">
            No results found for "{{ query }}"
        </div>
    {% endif %}
</div>
```

#### Dynamic Content Loading
```html
<!-- templates/parks/partials/rides_section.html -->
<div id="park-rides-section">
    <div class="flex justify-between items-center mb-6">
        <h2 class="text-2xl font-bold">Rides ({{ rides.count }})</h2>
        {% if can_edit %}
            <button hx-get="{% url 'ride-create' %}?park={{ park.slug }}"
                    hx-target="#ride-form-modal"
                    hx-swap="innerHTML"
                    class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
                Add Ride
            </button>
        {% endif %}
    </div>
    
    <!-- Filter Controls -->
    <div class="mb-6" x-data="{ filterOpen: false }">
        <button @click="filterOpen = !filterOpen" 
                class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">
            <span>Filters</span>
            <svg class="w-4 h-4 transform transition-transform" :class="{ 'rotate-180': filterOpen }">
                <!-- Chevron down icon -->
            </svg>
        </button>
        
        <div x-show="filterOpen" x-transition class="mt-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
            <form hx-get="{% url 'park-rides-partial' park.slug %}"
                  hx-target="#rides-grid"
                  hx-trigger="change"
                  class="grid grid-cols-1 md:grid-cols-3 gap-4">
                <select name="ride_type" class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700">
                    <option value="">All Types</option>
                    {% for value, label in ride_type_choices %}
                        <option value="{{ value }}" {% if request.GET.ride_type == value %}selected{% endif %}>
                            {{ label }}
                        </option>
                    {% endfor %}
                </select>
                
                <select name="status" class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700">
                    <option value="">All Statuses</option>
                    {% for value, label in status_choices %}
                        <option value="{{ value }}" {% if request.GET.status == value %}selected{% endif %}>
                            {{ label }}
                        </option>
                    {% endfor %}
                </select>
                
                <select name="sort" class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700">
                    <option value="name">Name A-Z</option>
                    <option value="-name">Name Z-A</option>
                    <option value="opening_date">Oldest First</option>
                    <option value="-opening_date">Newest First</option>
                </select>
            </form>
        </div>
    </div>
    
    <!-- Rides Grid -->
    <div id="rides-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {% for ride in rides %}
            {% include 'rides/partials/ride_card.html' with ride=ride %}
        {% endfor %}
    </div>
    
    <!-- Load More -->
    {% if has_next_page %}
        <div class="text-center mt-8">
            <button hx-get="{% url 'park-rides-partial' park.slug %}?page={{ page_number|add:1 }}"
                    hx-target="#rides-grid"
                    hx-swap="beforeend"
                    class="bg-gray-600 text-white px-6 py-2 rounded-lg hover:bg-gray-700">
                Load More Rides
            </button>
        </div>
    {% endif %}
</div>

<!-- Modal Container -->
<div id="ride-form-modal"></div>
```

### Form Integration with HTMX

#### Dynamic Form Handling
```html
<!-- templates/parks/partials/form.html -->
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" 
     x-data="{ show: true }" 
     x-show="show" 
     x-transition>
    <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-2xl mx-4">
        <div class="flex justify-between items-center mb-6">
            <h2 class="text-2xl font-bold">
                {% if park %}Edit Park{% else %}Add Park{% endif %}
            </h2>
            <button @click="show = false" 
                    hx-get="" 
                    hx-target="#park-form-modal" 
                    hx-swap="innerHTML"
                    class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
                ×
            </button>
        </div>
        
        <form hx-post="{% if park %}{% url 'park-edit' park.slug %}{% else %}{% url 'park-create' %}{% endif %}"
              hx-target="#park-form-modal"
              hx-swap="innerHTML"
              class="space-y-6">
            {% csrf_token %}
            
            <!-- Name Field -->
            <div>
                <label for="{{ form.name.id_for_label }}" class="block text-sm font-medium mb-2">
                    {{ form.name.label }}
                </label>
                {{ form.name }}
                {% if form.name.errors %}
                    <div class="mt-1 text-sm text-red-600 dark:text-red-400">
                        {{ form.name.errors.0 }}
                    </div>
                {% endif %}
            </div>
            
            <!-- Description Field -->
            <div>
                <label for="{{ form.description.id_for_label }}" class="block text-sm font-medium mb-2">
                    {{ form.description.label }}
                </label>
                {{ form.description }}
                {% if form.description.errors %}
                    <div class="mt-1 text-sm text-red-600 dark:text-red-400">
                        {{ form.description.errors.0 }}
                    </div>
                {% endif %}
            </div>
            
            <!-- Operator Field with Autocomplete -->
            <div>
                <label for="{{ form.operator.id_for_label }}" class="block text-sm font-medium mb-2">
                    {{ form.operator.label }}
                </label>
                <input type="text"
                       name="operator_search"
                       placeholder="Search operators..."
                       class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700"
                       hx-get="{% url 'ac-operators' %}"
                       hx-trigger="keyup changed delay:300ms"
                       hx-target="#operator-suggestions"
                       autocomplete="off">
                <div id="operator-suggestions" class="relative"></div>
                {{ form.operator.as_hidden }}
                {% if form.operator.errors %}
                    <div class="mt-1 text-sm text-red-600 dark:text-red-400">
                        {{ form.operator.errors.0 }}
                    </div>
                {% endif %}
            </div>
            
            <!-- Submit Buttons -->
            <div class="flex space-x-4">
                <button type="submit" 
                        class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500">
                    {% if park %}Update Park{% else %}Create Park{% endif %}
                </button>
                <button type="button" 
                        @click="show = false"
                        hx-get="" 
                        hx-target="#park-form-modal" 
                        hx-swap="innerHTML"
                        class="bg-gray-600 text-white px-6 py-2 rounded-lg hover:bg-gray-700">
                    Cancel
                </button>
            </div>
        </form>
    </div>
</div>
```

## Static Asset Management

### Tailwind CSS Configuration
```javascript
// tailwind.config.js
module.exports = {
  content: [
    './templates/**/*.html',
    './*/templates/**/*.html',
    './static/js/**/*.js',
  ],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
          900: '#1e3a8a',
        }
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      },
      animation: {
        'fade-in': 'fadeIn 0.3s ease-in-out',
        'slide-up': 'slideUp 0.3s ease-out',
      },
      keyframes: {
        fadeIn: {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        slideUp: {
          '0%': { transform: 'translateY(10px)', opacity: '0' },
          '100%': { transform: 'translateY(0)', opacity: '1' },
        },
      }
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
  ],
}
```

### Static Files Structure
```
static/
├── css/
│   ├── src/
│   │   ├── main.css              # Tailwind source
│   │   ├── components.css        # Custom components
│   │   └── utilities.css         # Custom utilities
│   └── styles.css                # Compiled output
├── js/
│   ├── main.js                   # Main JavaScript
│   ├── components/
│   │   ├── autocomplete.js       # Autocomplete functionality
│   │   ├── modal.js              # Modal management
│   │   └── theme-toggle.js       # Dark mode toggle
│   └── vendor/
│       ├── htmx.min.js          # HTMX library
│       └── alpine.min.js        # Alpine.js library
└── images/
    ├── placeholders/
    │   ├── park-placeholder.jpg
    │   └── ride-placeholder.jpg
    └── icons/
        ├── logo.svg
        └── social-icons/
```

### Custom CSS Components
```css
/* static/css/src/components.css */
@layer components {
  .btn {
    @apply px-4 py-2 rounded-lg font-medium transition-colors focus:outline-none focus:ring-2;
  }
  
  .btn-primary {
    @apply btn bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500;
  }
  
  .btn-secondary {
    @apply btn bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500;
  }
  
  .card {
    @apply bg-white dark:bg-gray-800 rounded-lg shadow-md border border-gray-200 dark:border-gray-700;
  }
  
  .card-header {
    @apply px-6 py-4 border-b border-gray-200 dark:border-gray-700;
  }
  
  .card-body {
    @apply px-6 py-4;
  }
  
  .form-input {
    @apply w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100;
  }
  
  .alert {
    @apply px-4 py-3 rounded-lg border;
  }
  
  .alert-success {
    @apply alert bg-green-50 border-green-200 text-green-800 dark:bg-green-900 dark:border-green-700 dark:text-green-200;
  }
  
  .alert-error {
    @apply alert bg-red-50 border-red-200 text-red-800 dark:bg-red-900 dark:border-red-700 dark:text-red-200;
  }
  
  .htmx-indicator {
    @apply opacity-0 transition-opacity;
  }
  
  .htmx-request .htmx-indicator {
    @apply opacity-100;
  }
  
  .htmx-request.htmx-indicator {
    @apply opacity-100;
  }
}
```

## JavaScript Architecture

### HTMX Configuration
```javascript
// static/js/main.js
document.addEventListener('DOMContentLoaded', function() {
    // HTMX Global Configuration
    htmx.config.defaultSwapStyle = 'innerHTML';
    htmx.config.scrollBehavior = 'smooth';
    htmx.config.requestClass = 'htmx-request';
    htmx.config.addedClass = 'htmx-added';
    htmx.config.settledClass = 'htmx-settled';
    
    // Global HTMX event handlers
    document.body.addEventListener('htmx:configRequest', function(evt) {
        evt.detail.headers['X-CSRFToken'] = getCSRFToken();
        evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
    });
    
    document.body.addEventListener('htmx:beforeSwap', function(evt) {
        // Handle error responses
        if (evt.detail.xhr.status === 400) {
            // Keep form visible to show validation errors
            evt.detail.shouldSwap = true;
        } else if (evt.detail.xhr.status === 403) {
            // Show permission denied message
            showAlert('Permission denied', 'error');
            evt.detail.shouldSwap = false;
        } else if (evt.detail.xhr.status >= 500) {
            // Show server error message
            showAlert('Server error occurred', 'error');
            evt.detail.shouldSwap = false;
        }
    });
    
    document.body.addEventListener('htmx:afterSwap', function(evt) {
        // Re-initialize any JavaScript components in swapped content
        initializeComponents(evt.detail.target);
    });
    
    // Initialize components on page load
    initializeComponents(document);
});

function getCSRFToken() {
    return document.querySelector('[name=csrfmiddlewaretoken]')?.value || 
           document.querySelector('meta[name=csrf-token]')?.getAttribute('content');
}

function initializeComponents(container) {
    // Initialize any JavaScript components that need setup
    container.querySelectorAll('[data-component]').forEach(el => {
        const component = el.dataset.component;
        if (window.components && window.components[component]) {
            window.components[component](el);
        }
    });
}

function showAlert(message, type = 'info') {
    const alertContainer = document.getElementById('messages') || createAlertContainer();
    const alert = document.createElement('div');
    alert.className = `alert alert-${type} mb-2 animate-fade-in`;
    alert.innerHTML = `
        ${message}
        <button onclick="this.parentElement.remove()" class="ml-2 hover:text-opacity-75">×</button>
    `;
    alertContainer.appendChild(alert);
    
    // Auto-remove after 5 seconds
    setTimeout(() => {
        if (alert.parentElement) {
            alert.remove();
        }
    }, 5000);
}
```

### Component System
```javascript
// static/js/components/autocomplete.js
window.components = window.components || {};

window.components.autocomplete = function(element) {
    const input = element.querySelector('input');
    const resultsContainer = element.querySelector('.autocomplete-results');
    let currentFocus = -1;
    
    input.addEventListener('keydown', function(e) {
        const items = resultsContainer.querySelectorAll('.autocomplete-item');
        
        if (e.key === 'ArrowDown') {
            e.preventDefault();
            currentFocus = Math.min(currentFocus + 1, items.length - 1);
            updateActiveItem(items);
        } else if (e.key === 'ArrowUp') {
            e.preventDefault();
            currentFocus = Math.max(currentFocus - 1, -1);
            updateActiveItem(items);
        } else if (e.key === 'Enter') {
            e.preventDefault();
            if (currentFocus >= 0 && items[currentFocus]) {
                items[currentFocus].click();
            }
        } else if (e.key === 'Escape') {
            resultsContainer.innerHTML = '';
            currentFocus = -1;
        }
    });
    
    function updateActiveItem(items) {
        items.forEach((item, index) => {
            item.classList.toggle('bg-blue-50', index === currentFocus);
        });
    }
};
```

## Template Tags and Filters

### Custom Template Tags
```python
# parks/templatetags/parks_tags.py
from django import template
from django.utils.html import format_html
from django.urls import reverse

register = template.Library()

@register.simple_tag
def ride_type_icon(ride_type):
    """Return icon class for ride type"""
    icons = {
        'RC': 'fas fa-roller-coaster',
        'DR': 'fas fa-ghost',
        'FR': 'fas fa-circle',
        'WR': 'fas fa-water',
        'TR': 'fas fa-train',
        'OT': 'fas fa-star',
    }
    return icons.get(ride_type, 'fas fa-question')

@register.simple_tag
def status_badge(status):
    """Return colored badge for status"""
    colors = {
        'OPERATING': 'bg-green-100 text-green-800',
        'CLOSED_TEMP': 'bg-yellow-100 text-yellow-800',
        'CLOSED_PERM': 'bg-red-100 text-red-800',
        'UNDER_CONSTRUCTION': 'bg-blue-100 text-blue-800',
        'DEMOLISHED': 'bg-gray-100 text-gray-800',
        'RELOCATED': 'bg-purple-100 text-purple-800',
    }
    color_class = colors.get(status, 'bg-gray-100 text-gray-800')
    display_text = status.replace('_', ' ').title()
    
    return format_html(
        '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {}">{}</span>',
        color_class,
        display_text
    )

@register.inclusion_tag('parks/partials/ride_card.html')
def ride_card(ride, show_park=False):
    """Render a ride card component"""
    return {
        'ride': ride,
        'show_park': show_park,
    }

@register.filter
def duration_format(seconds):
    """Format duration in seconds to human readable"""
    if not seconds:
        return ''
    
    minutes = seconds // 60
    remaining_seconds = seconds % 60
    
    if minutes > 0:
        return f"{minutes}:{remaining_seconds:02d}"
    else:
        return f"{seconds}s"
```

## Conversion to Symfony Twig

### Template Structure Mapping

| Django Template | Symfony Twig Equivalent |
|----------------|-------------------------|
| `templates/base/base.html` | `templates/base.html.twig` |
| `{% extends 'base.html' %}` | `{% extends 'base.html.twig' %}` |
| `{% block content %}` | `{% block content %}` |
| `{% include 'partial.html' %}` | `{% include 'partial.html.twig' %}` |
| `{% url 'route-name' %}` | `{{ path('route_name') }}` |
| `{% static 'file.css' %}` | `{{ asset('file.css') }}` |
| `{% csrf_token %}` | `{{ csrf_token() }}` |
| `{% if user.is_authenticated %}` | `{% if is_granted('ROLE_USER') %}` |

### Twig Template Example
```twig
{# templates/parks/detail.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}{{ park.name }} - ThrillWiki{% endblock %}

{% block content %}
<div class="container mx-auto px-4 py-8">
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
        <!-- Main Content -->
        <div class="lg:col-span-2">
            <div class="card">
                <div class="card-header">
                    <div class="flex justify-between items-start">
                        <div>
                            <h1 class="text-3xl font-bold">{{ park.name }}</h1>
                            <p class="text-gray-600 dark:text-gray-400 mt-1">
                                Operated by 
                                <a href="{{ path('operator_detail', {slug: park.operator.slug}) }}" 
                                   class="text-blue-600 hover:underline">
                                    {{ park.operator.name }}
                                </a>
                            </p>
                        </div>
                        {{ status_badge(park.status) }}
                    </div>
                </div>
                
                <div class="card-body">
                    {% if park.description %}
                        <p class="text-gray-700 dark:text-gray-300 mb-6">
                            {{ park.description }}
                        </p>
                    {% endif %}
                    
                    <!-- Tabs -->
                    <div x-data="{ activeTab: 'rides' }" class="mt-8">
                        <div class="border-b border-gray-200 dark:border-gray-700">
                            <nav class="-mb-px flex space-x-8">
                                <button @click="activeTab = 'rides'"
                                        :class="{ 'border-blue-500 text-blue-600': activeTab === 'rides' }"
                                        class="py-2 px-1 border-b-2 border-transparent font-medium text-sm hover:text-gray-700 hover:border-gray-300">
                                    Rides ({{ park.rides|length }})
                                </button>
                                <button @click="activeTab = 'photos'"
                                        :class="{ 'border-blue-500 text-blue-600': activeTab === 'photos' }"
                                        class="py-2 px-1 border-b-2 border-transparent font-medium text-sm hover:text-gray-700 hover:border-gray-300">
                                    Photos ({{ park.photos|length }})
                                </button>
                                <button @click="activeTab = 'reviews'"
                                        :class="{ 'border-blue-500 text-blue-600': activeTab === 'reviews' }"
                                        class="py-2 px-1 border-b-2 border-transparent font-medium text-sm hover:text-gray-700 hover:border-gray-300">
                                    Reviews ({{ park.reviews|length }})
                                </button>
                            </nav>
                        </div>
                        
                        <!-- Tab Content -->
                        <div class="mt-6">
                            <div x-show="activeTab === 'rides'"
                                 hx-get="{{ path('park_rides_partial', {slug: park.slug}) }}"
                                 hx-trigger="revealed once">
                                Loading rides...
                            </div>
                            
                            <div x-show="activeTab === 'photos'"
                                 hx-get="{{ path('park_photos_partial', {slug: park.slug}) }}"
                                 hx-trigger="revealed once">
                                Loading photos...
                            </div>
                            
                            <div x-show="activeTab === 'reviews'"
                                 hx-get="{{ path('park_reviews_partial', {slug: park.slug}) }}"
                                 hx-trigger="revealed once">
                                Loading reviews...
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        
        <!-- Sidebar -->
        <div class="space-y-6">
            {% include 'parks/partials/park_info.html.twig' %}
            {% include 'parks/partials/park_stats.html.twig' %}
        </div>
    </div>
</div>
{% endblock %}
```

## Asset Management Migration

### Symfony Asset Strategy
```yaml
# webpack.config.js (Symfony Webpack Encore)
const Encore = require('@symfony/webpack-encore');

Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')
    .addEntry('app', './assets/app.js')
    .addEntry('admin', './assets/admin.js')
    .addStyleEntry('styles', './assets/styles/app.css')
    
    // Enable PostCSS for Tailwind
    .enablePostCssLoader()
    
    // Enable source maps in dev
    .enableSourceMaps(!Encore.isProduction())
    
    // Enable versioning in production
    .enableVersioning(Encore.isProduction())
    
    // Configure Babel
    .configureBabelPresetEnv((config) => {
        config.useBuiltIns = 'usage';
        config.corejs = 3;
    })
    
    // Copy static assets
    .copyFiles({
        from: './assets/images',
        to: 'images/[path][name].[hash:8].[ext]'
    });

module.exports = Encore.getWebpackConfig();
```

## Next Steps for Frontend Conversion

1. **Template Migration** - Convert Django templates to Twig syntax
2. **Asset Pipeline** - Set up Symfony Webpack Encore with Tailwind
3. **HTMX Integration** - Ensure HTMX works with Symfony controllers
4. **Component System** - Migrate JavaScript components to work with Twig
5. **Styling Migration** - Adapt Tailwind configuration for Symfony structure
6. **Template Functions** - Create Twig extensions for custom template tags
7. **Form Theming** - Set up Symfony form themes to match current styling

---

**Status:** ✅ **COMPLETED** - Frontend architecture analysis for Symfony conversion  
**Next:** Database schema analysis and migration planning