# 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 {% block title %}ThrillWiki{% endblock %} {% block extra_head %}{% endblock %} {% include 'base/navigation.html' %}
{% if messages %}
{% for message in messages %}
{{ message }}
{% endfor %}
{% endif %} {% block content %}{% endblock %}
{% include 'base/footer.html' %} {% block extra_scripts %}{% endblock %} ``` #### Navigation Component ```html ``` ### HTMX Integration Patterns #### Autocomplete Component ```html
{% if results.parks or results.rides %} {% if results.parks %}
Parks
{% for park in results.parks %}
{{ park.name }}
{{ park.operator.name }} • {{ park.status|title }}
{% endfor %}
{% endif %} {% if results.rides %}
Rides
{% for ride in results.rides %}
{{ ride.name }}
{{ ride.park.name }} • {{ ride.get_ride_type_display }}
{% endfor %}
{% endif %} {% else %}
No results found for "{{ query }}"
{% endif %}
``` #### Dynamic Content Loading ```html

Rides ({{ rides.count }})

{% if can_edit %} {% endif %}
{% for ride in rides %} {% include 'rides/partials/ride_card.html' with ride=ride %} {% endfor %}
{% if has_next_page %}
{% endif %}
``` ### Form Integration with HTMX #### Dynamic Form Handling ```html

{% if park %}Edit Park{% else %}Add Park{% endif %}

{% csrf_token %}
{{ form.name }} {% if form.name.errors %}
{{ form.name.errors.0 }}
{% endif %}
{{ form.description }} {% if form.description.errors %}
{{ form.description.errors.0 }}
{% endif %}
{{ form.operator.as_hidden }} {% if form.operator.errors %}
{{ form.operator.errors.0 }}
{% endif %}
``` ## 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} `; 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( '{}', 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 %}

{{ park.name }}

Operated by {{ park.operator.name }}

{{ status_badge(park.status) }}
{% if park.description %}

{{ park.description }}

{% endif %}
Loading rides...
Loading photos...
Loading reviews...
{% include 'parks/partials/park_info.html.twig' %} {% include 'parks/partials/park_stats.html.twig' %}
{% 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