Files
thrillwiki_django_no_react/shared/docs/THRILLWIKI_PROJECT_DOCUMENTATION.md
pacnpal d504d41de2 feat: complete monorepo structure with frontend and shared resources
- Add complete backend/ directory with full Django application
- Add frontend/ directory with Vite + TypeScript setup ready for Next.js
- Add comprehensive shared/ directory with:
  - Complete documentation and memory-bank archives
  - Media files and avatars (letters, park/ride images)
  - Deployment scripts and automation tools
  - Shared types and utilities
- Add architecture/ directory with migration guides
- Configure pnpm workspace for monorepo development
- Update .gitignore to exclude .django_tailwind_cli/ build artifacts
- Preserve all historical documentation in shared/docs/memory-bank/
- Set up proper structure for full-stack development with shared resources
2025-08-23 18:40:07 -04:00

55 KiB

ThrillWiki Project Documentation

Table of Contents

  1. Project Overview
  2. Technical Stack and Architecture
  3. Database Models and Relationships
  4. Visual Theme and Design System
  5. Frontend Implementation Patterns
  6. User Experience and Key Features
  7. Page Structure and Templates
  8. Services and Business Logic
  9. Development Workflow
  10. API Endpoints and URL Structure

Project Overview

ThrillWiki is a sophisticated Django-based web application designed for theme park and roller coaster enthusiasts. It provides comprehensive information management for parks, rides, companies, and user-generated content with advanced features including geographic mapping, moderation systems, and real-time interactions.

Key Characteristics

  • Enterprise-Grade Architecture: Service-oriented design with clear separation of concerns
  • Modern Frontend: HTMX + Alpine.js for dynamic interactions without heavy JavaScript frameworks
  • Geographic Intelligence: PostGIS integration for location-based features and mapping
  • Content Moderation: Comprehensive workflow for user-generated content approval
  • Audit Trail: Complete history tracking using django-pghistory
  • Responsive Design: Mobile-first approach with sophisticated dark theme support

Technical Stack and Architecture

Core Technologies

Component Technology Version Purpose
Backend Framework Django 5.0+ Main web framework
Database PostgreSQL + PostGIS Latest Relational database with geographic extension
Frontend HTMX + Alpine.js 1.9.6 + Latest Dynamic interactions and client-side behavior
Styling Tailwind CSS Latest Utility-first CSS framework
Package Manager UV Latest Python dependency management
Authentication Django Allauth 0.60.1+ OAuth and user management
History Tracking django-pghistory 3.5.2+ Audit trails and versioning
Testing Pytest + Playwright Latest Unit and E2E testing

Architecture Patterns

Service-Oriented Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Presentation  │    │    Business     │    │      Data       │
│     Layer       │    │     Logic       │    │     Layer       │
├─────────────────┤    ├─────────────────┤    ├─────────────────┤
│ • Templates     │◄──►│ • Services      │◄──►│ • Models        │
│ • Views         │    │ • Map Service   │    │ • Database      │
│ • HTMX/Alpine   │    │ • Search        │    │ • PostGIS       │
│ • Tailwind CSS  │    │ • Moderation    │    │ • Caching       │
└─────────────────┘    └─────────────────┘    └─────────────────┘

Django App Organization

The project follows a domain-driven design approach with clear app boundaries:

thrillwiki_django_no_react/
├── core/                   # Core business logic and shared services
│   ├── services/          # Unified map service, clustering, caching
│   ├── search/            # Search functionality
│   ├── mixins/            # Reusable view mixins
│   └── history/           # History tracking utilities
├── accounts/              # User management and authentication
├── parks/                 # Theme park entities
│   └── models/            # Park, Company, Location models
├── rides/                 # Ride entities and ride-specific logic
│   └── models/            # Ride, RideModel, Company models
├── location/              # Geographic location handling
├── media/                 # Media file management and photo handling
├── moderation/            # Content moderation workflow
├── email_service/         # Email handling and notifications
└── static/                # Frontend assets (CSS, JS, images)

Package Management with UV

ThrillWiki exclusively uses UV for Python package management, providing:

  • Faster dependency resolution: Significantly faster than pip
  • Lock file support: Ensures reproducible environments
  • Virtual environment management: Automatic environment handling
  • Cross-platform compatibility: Consistent behavior across development environments

Critical Commands

# Add new dependencies
uv add <package>

# Django management (NEVER use python manage.py)
uv run manage.py <command>

# Development server startup
lsof -ti :8000 | xargs kill -9; find . -type d -name "__pycache__" -exec rm -r {} +; uv run manage.py tailwind runserver

Database Models and Relationships

Entity Relationship Architecture

ThrillWiki implements a sophisticated entity relationship model that enforces business rules at the database level:

Core Business Rules (from .clinerules)

  1. Park Relationships

    • Parks MUST have an Operator (required relationship)
    • Parks MAY have a PropertyOwner (optional, usually same as Operator)
    • Parks CANNOT directly reference Company entities
  2. Ride Relationships

    • Rides MUST belong to a Park (required relationship)
    • Rides MAY have a Manufacturer (optional relationship)
    • Rides MAY have a Designer (optional relationship)
    • Rides CANNOT directly reference Company entities
  3. Entity Definitions

    • Operators: Companies that operate theme parks
    • PropertyOwners: Companies that own park property (new concept)
    • Manufacturers: Companies that manufacture rides
    • Designers: Companies/individuals that design rides

Core Model Structure

Park Models (parks/models/)

# Park Entity
class Park(TrackedModel):
    # Core identifiers
    name = CharField(max_length=255)
    slug = SlugField(max_length=255, unique=True)
    
    # Business relationships (enforced by .clinerules)
    operator = ForeignKey('Company', related_name='operated_parks')
    property_owner = ForeignKey('Company', related_name='owned_parks', null=True)
    
    # Operational data
    status = CharField(choices=STATUS_CHOICES, default="OPERATING")
    opening_date = DateField(null=True, blank=True)
    size_acres = DecimalField(max_digits=10, decimal_places=2)
    
    # Statistics
    average_rating = DecimalField(max_digits=3, decimal_places=2)
    ride_count = IntegerField(null=True, blank=True)
    coaster_count = IntegerField(null=True, blank=True)

Ride Models (rides/models/)

# Ride Entity
class Ride(TrackedModel):
    # Core identifiers
    name = CharField(max_length=255)
    slug = SlugField(max_length=255)
    
    # Required relationships (enforced by .clinerules)
    park = ForeignKey('parks.Park', related_name='rides')
    
    # Optional business relationships
    manufacturer = ForeignKey('Company', related_name='manufactured_rides')
    designer = ForeignKey('Company', related_name='designed_rides')
    ride_model = ForeignKey('RideModel', related_name='rides')
    
    # Classification
    category = CharField(choices=CATEGORY_CHOICES)
    status = CharField(choices=STATUS_CHOICES, default='OPERATING')

Company Models (Shared across apps)

# Company Entity (supports multiple roles)
class Company(TrackedModel):
    class CompanyRole(TextChoices):
        OPERATOR = 'OPERATOR', 'Park Operator'
        PROPERTY_OWNER = 'PROPERTY_OWNER', 'Property Owner'
        MANUFACTURER = 'MANUFACTURER', 'Ride Manufacturer'
        DESIGNER = 'DESIGNER', 'Ride Designer'
    
    name = CharField(max_length=255)
    slug = SlugField(max_length=255, unique=True)
    roles = ArrayField(CharField(choices=CompanyRole.choices))

Geographic Models (location/models/)

# Generic Location Model
class Location(TrackedModel):
    # Generic relationship (can attach to any model)
    content_type = ForeignKey(ContentType)
    object_id = PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    
    # Geographic data (dual storage for compatibility)
    latitude = DecimalField(max_digits=9, decimal_places=6)
    longitude = DecimalField(max_digits=9, decimal_places=6)
    point = PointField(srid=4326)  # PostGIS geometry field
    
    # Address components
    street_address = CharField(max_length=255)
    city = CharField(max_length=100)
    state = CharField(max_length=100)
    country = CharField(max_length=100)
    postal_code = CharField(max_length=20)

History Tracking with pghistory

Every critical model uses @pghistory.track() decoration for comprehensive audit trails:

@pghistory.track()
class Park(TrackedModel):
    # All field changes are automatically tracked
    # Creates parallel history tables with full change logs

Media and Content Models

# Generic Photo Model
class Photo(TrackedModel):
    # Generic relationship support
    content_type = ForeignKey(ContentType)
    object_id = PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    
    # Media handling
    image = ImageField(upload_to=photo_upload_path, storage=MediaStorage())
    is_primary = BooleanField(default=False)
    is_approved = BooleanField(default=False)
    
    # Metadata extraction
    date_taken = DateTimeField(null=True)  # Auto-extracted from EXIF
    uploaded_by = ForeignKey(User, related_name='uploaded_photos')

User and Authentication Models

# Extended User Model
class User(AbstractUser):
    class Roles(TextChoices):
        USER = 'USER', 'User'
        MODERATOR = 'MODERATOR', 'Moderator'
        ADMIN = 'ADMIN', 'Admin'
        SUPERUSER = 'SUPERUSER', 'Superuser'
    
    # Immutable identifier
    user_id = CharField(max_length=10, unique=True, editable=False)
    
    # Permission system
    role = CharField(choices=Roles.choices, default=Roles.USER)
    
    # User preferences
    theme_preference = CharField(choices=ThemePreference.choices)

Visual Theme and Design System

Design Philosophy

ThrillWiki implements a sophisticated dark-first design system with vibrant accent colors that reflect the excitement of theme parks and roller coasters.

Color Palette

:root {
  --primary: #4f46e5;     /* Vibrant indigo */
  --secondary: #e11d48;   /* Vibrant rose */
  --accent: #8b5cf6;      /* Purple accent */
}

Background Gradients

/* Light theme */
body {
  background: linear-gradient(to bottom right, 
    white, 
    rgb(239 246 255), /* blue-50 */
    rgb(238 242 255)  /* indigo-50 */
  );
}

/* Dark theme */
body.dark {
  background: linear-gradient(to bottom right,
    rgb(3 7 18),      /* gray-950 */
    rgb(49 46 129),   /* indigo-950 */
    rgb(59 7 100)     /* purple-950 */
  );
}

Tailwind CSS Configuration

Custom Configuration (tailwind.config.js)

module.exports = {
  darkMode: 'class',  // Class-based dark mode
  content: [
    './templates/**/*.html',
    './assets/css/src/**/*.css',
  ],
  theme: {
    extend: {
      colors: {
        primary: '#4f46e5',
        secondary: '#e11d48', 
        accent: '#8b5cf6'
      },
      fontFamily: {
        'sans': ['Poppins', 'sans-serif'],
      },
    },
  },
  plugins: [
    require("@tailwindcss/typography"),
    require("@tailwindcss/forms"),
    require("@tailwindcss/aspect-ratio"),
    require("@tailwindcss/container-queries"),
    // Custom HTMX variants
    plugin(function ({ addVariant }) {
      addVariant("htmx-settling", ["&.htmx-settling", ".htmx-settling &"]);
      addVariant("htmx-request", ["&.htmx-request", ".htmx-request &"]);
      addVariant("htmx-swapping", ["&.htmx-swapping", ".htmx-swapping &"]);
      addVariant("htmx-added", ["&.htmx-added", ".htmx-added &"]);
    }),
  ],
}

Component System (static/css/src/input.css)

Button Components

.btn-primary {
  @apply inline-flex items-center px-6 py-2.5 border border-transparent 
         rounded-full shadow-md text-sm font-medium text-white 
         bg-gradient-to-r from-primary to-secondary 
         hover:from-primary/90 hover:to-secondary/90 
         focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary/50 
         transform hover:scale-105 transition-all;
}

.btn-secondary {
  @apply inline-flex items-center px-6 py-2.5 border border-gray-200 
         dark:border-gray-700 rounded-full shadow-md text-sm font-medium 
         text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 
         hover:bg-gray-50 dark:hover:bg-gray-700 
         transform hover:scale-105 transition-all;
}

Navigation Components

.nav-link {
  @apply flex items-center text-gray-700 dark:text-gray-200 
         px-6 py-2.5 rounded-lg font-medium border border-transparent 
         hover:border-primary/20 dark:hover:border-primary/30
         hover:text-primary dark:text-primary 
         hover:bg-primary/10 dark:bg-primary/20;
}

Card System

.card {
  @apply p-6 bg-white dark:bg-gray-800 border rounded-lg shadow-lg 
         border-gray-200/50 dark:border-gray-700/50;
}

.card-hover {
  @apply transition-transform transform hover:-translate-y-1;
}

Responsive Grid System

/* Adaptive grid with content-aware sizing */
.grid-adaptive {
  @apply grid gap-6;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

/* Stats grid with even layouts */
.grid-stats {
  @apply grid gap-4;
  grid-template-columns: repeat(2, 1fr);  /* Mobile: 2 columns */
}

@media (min-width: 1024px) {
  .grid-stats {
    grid-template-columns: repeat(3, 1fr);  /* Desktop: 3 columns */
  }
}

@media (min-width: 1280px) {
  .grid-stats {
    grid-template-columns: repeat(5, 1fr);  /* Large: 5 columns */
  }
}

Dark Mode Implementation

Theme Toggle System

The theme system provides:

  • Automatic detection of system preference
  • Manual toggle with persistent storage
  • Flash prevention during page load
  • Smooth transitions between themes
// Theme initialization (prevents flash)
let theme = localStorage.getItem("theme");
if (!theme) {
  theme = window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark" : "light";
  localStorage.setItem("theme", theme);
}
if (theme === "dark") {
  document.documentElement.classList.add("dark");
}

CSS Custom Properties for Theme Switching

/* Theme-aware components */
.auth-card {
  @apply w-full max-w-md p-8 mx-auto border shadow-xl 
         bg-white/90 dark:bg-gray-800/90 rounded-2xl backdrop-blur-sm 
         border-gray-200/50 dark:border-gray-700/50;
}

/* Status badges with theme support */
.status-operating {
  @apply text-green-800 bg-green-100 dark:bg-green-700 dark:text-green-50;
}

.status-closed {
  @apply text-red-800 bg-red-100 dark:bg-red-700 dark:text-red-50;
}

Frontend Implementation Patterns

HTMX Integration Patterns

ThrillWiki leverages HTMX for dynamic interactions while maintaining server-side rendering benefits:

Dynamic Content Loading

<!-- Search with live results -->
<form hx-get="{% url 'parks:search' %}" 
      hx-target="#search-results" 
      hx-trigger="input changed delay:300ms">
  <input type="text" name="q" placeholder="Search parks and rides...">
</form>

<div id="search-results" 
     hx-indicator="#loading-spinner">
  <!-- Results loaded dynamically -->
</div>

Modal Interactions

<!-- HTMX-powered modals -->
<div hx-get="{% url 'account_login' %}"
     hx-target="body"
     hx-swap="beforeend"
     class="cursor-pointer menu-item">
  <span>Login</span>
</div>

Custom HTMX Variants

/* Loading states */
.htmx-request .htmx-indicator {
  display: block;
}

/* Transition effects */
.htmx-settling {
  opacity: 0.7;
  transition: opacity 0.3s ease;
}

.htmx-swapping {
  transform: scale(0.98);
  transition: transform 0.2s ease;
}

Alpine.js Patterns

Alpine.js handles client-side state management and interactions:

Dropdown Components

<div x-data="{ open: false }" 
     @click.outside="open = false">
  <!-- Profile dropdown -->
  <img @click="open = !open" 
       src="{{ user.profile.avatar.url }}" 
       class="cursor-pointer">
  
  <div x-cloak
       x-show="open"
       x-transition:enter="transition ease-out duration-100"
       x-transition:enter-start="transform opacity-0 scale-95"
       x-transition:enter-end="transform opacity-100 scale-100">
    <!-- Dropdown content -->
  </div>
</div>

Modal Management

<div x-data="{ 
       show: false, 
       editingPhoto: null,
       init() { this.editingPhoto = { caption: '' }; }
     }"
     @show-photo-upload.window="show = true; init()">
  <!-- Modal implementation -->
</div>

JavaScript Architecture (static/js/)

Modular JavaScript Organization

static/js/
├── main.js              # Core functionality (theme, navigation)
├── alerts.js            # Alert system management
├── photo-gallery.js     # Photo gallery interactions
├── park-map.js          # Leaflet map integration
├── location-autocomplete.js  # Geographic search
└── alpine.min.js        # Alpine.js framework

Theme Management (static/js/main.js)

// Theme handling with system preference detection
document.addEventListener('DOMContentLoaded', () => {
  const themeToggle = document.getElementById('theme-toggle');
  const html = document.documentElement;
  
  // Initialize toggle state
  if (themeToggle) {
    themeToggle.checked = html.classList.contains('dark');
    
    // Handle toggle changes
    themeToggle.addEventListener('change', function() {
      const isDark = this.checked;
      html.classList.toggle('dark', isDark);
      localStorage.setItem('theme', isDark ? 'dark' : 'light');
    });
    
    // Listen for system theme changes
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    mediaQuery.addEventListener('change', (e) => {
      if (!localStorage.getItem('theme')) {
        const isDark = e.matches;
        html.classList.toggle('dark', isDark);
        themeToggle.checked = isDark;
      }
    });
  }
});

Mobile Navigation

// Mobile menu with smooth transitions
const toggleMenu = () => {
  isMenuOpen = !isMenuOpen;
  mobileMenu.classList.toggle('show', isMenuOpen);
  mobileMenuBtn.setAttribute('aria-expanded', isMenuOpen.toString());
  
  // Update icon
  const icon = mobileMenuBtn.querySelector('i');
  icon.classList.remove(isMenuOpen ? 'fa-bars' : 'fa-times');
  icon.classList.add(isMenuOpen ? 'fa-times' : 'fa-bars');
};

Template System Patterns

Component-Based Architecture

templates/
├── base/
│   └── base.html            # Base template with navigation
├── core/
│   ├── search/
│   │   ├── components/      # Search UI components
│   │   ├── layouts/         # Search layout templates
│   │   └── partials/        # Reusable search elements
├── parks/
│   ├── partials/
│   │   ├── park_list_item.html     # Reusable park card
│   │   ├── park_actions.html       # Action buttons
│   │   └── park_stats.html         # Statistics display
│   ├── park_detail.html            # Main park page
│   └── park_list.html              # Park listing page
└── media/
    └── partials/
        ├── photo_display.html      # Photo gallery component
        └── photo_upload.html       # Upload interface

Template Inheritance Pattern

<!-- base/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <title>{% block title %}ThrillWiki{% endblock %}</title>
  {% block extra_head %}{% endblock %}
</head>
<body class="bg-gradient-to-br from-white via-blue-50 to-indigo-50 
             dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950">
  
  {% include "base/navigation.html" %}
  
  <main class="container flex-grow px-6 py-8 mx-auto">
    {% block content %}{% endblock %}
  </main>
  
  {% block extra_js %}{% endblock %}
</body>
</html>

HTMX Template Patterns

<!-- Responsive template selection -->
{% extends "base/base.html" %}

{% block content %}
<div hx-get="{% url 'parks:park_list' %}"
     hx-target="#park-grid"
     hx-swap="innerHTML"
     hx-trigger="load, view-change from:body">
  
  <div id="park-grid" class="grid-adaptive">
    {% include "parks/partials/park_list_item.html" %}
  </div>
</div>
{% endblock %}

User Experience and Key Features

Navigation and Information Architecture

Primary Navigation Structure

ThrillWiki Navigation
├── Home                    # Dashboard with featured content
├── Parks                   # Theme park directory
│   ├── Browse Parks       # Filterable park listings
│   ├── Add New Park       # User contribution form
│   └── [Park Detail]      # Individual park pages
├── Rides                   # Ride directory (global)
│   ├── Browse Rides       # Cross-park ride search
│   ├── Add New Ride       # User contribution form
│   └── [Ride Detail]      # Individual ride pages
├── Search                  # Universal search interface
└── User Account
    ├── Profile            # User profile and stats
    ├── Settings           # Preferences and account
    ├── Moderation         # Content review (if authorized)
    └── Admin              # System administration (if authorized)

Responsive Navigation Patterns

  • Desktop: Full horizontal navigation with search bar
  • Tablet: Collapsible navigation with maintained search functionality
  • Mobile: Hamburger menu with slide-out panel

Core Feature Set

1. Park Management System

Park Detail Pages provide comprehensive information:

Park Information Hierarchy
├── Header Section
│   ├── Park name and location
│   ├── Status badge (Operating, Closed, etc.)
│   ├── Average rating display
│   └── Quick action buttons
├── Statistics Dashboard
│   ├── Operator information (priority display)
│   ├── Property owner (if different)
│   ├── Total ride count (linked)
│   ├── Roller coaster count
│   ├── Opening date
│   └── Website link
├── Content Sections
│   ├── Photo gallery (if photos exist)
│   ├── About section (description)
│   ├── Rides & Attractions (preview list)
│   └── Location map (if coordinates available)
└── Additional Information
    ├── History timeline
    ├── Related parks
    └── User contributions

Key UX Features:

  • Smart statistics layout: Responsive grid that prevents awkward spacing
  • Priority content placement: Operator information prominently featured
  • Contextual actions: Edit/moderate buttons appear based on user permissions
  • Progressive disclosure: Detailed information revealed as needed

2. Advanced Search and Filtering

Unified Search System supports:

  • Cross-content search: Parks, rides, companies in single interface
  • Geographic filtering: Search within specific regions or distances
  • Attribute filtering: Status, ride types, ratings, opening dates
  • Real-time results: HTMX-powered instant search feedback

Search Result Patterns:

<!-- Search results with context -->
<div class="search-result-item">
  <div class="result-type-badge">Park</div>
  <h3 class="result-title">{{ park.name }}</h3>
  <p class="result-location">{{ park.formatted_location }}</p>
  <div class="result-stats">
    <span>{{ park.ride_count }} rides</span>
    <span>{{ park.get_status_display }}</span>
  </div>
</div>

3. Geographic and Mapping Features

Unified Map Service provides:

  • Multi-layer mapping: Parks, rides, and companies on single map
  • Intelligent clustering: Zoom-level appropriate point grouping
  • Performance optimization: Smart caching and result limiting
  • Geographic bounds: Efficient spatial queries using PostGIS

Map Integration Pattern:

// Park detail map initialization
document.addEventListener('DOMContentLoaded', function() {
  {% with location=park.location.first %}
  initParkMap({{ location.latitude }}, {{ location.longitude }}, "{{ park.name }}");
  {% endwith %}
});

4. Content Moderation Workflow

Submission Process:

User Contribution Flow
├── Content Creation
│   ├── Form submission (parks, rides, photos)
│   ├── Validation and sanitization
│   └── EditSubmission/PhotoSubmission creation
├── Review Process
│   ├── Moderator dashboard listing
│   ├── Side-by-side comparison view
│   ├── Edit capability before approval
│   └── Approval/rejection with notes
├── Publication
│   ├── Automatic publication for moderators
│   ├── Content integration into main database
│   └── User notification system
└── History Tracking
    ├── Complete audit trail
    ├── Revert capability
    └── Performance metrics

Moderation Features:

  • Auto-approval: Moderators bypass review process
  • Edit before approval: Moderators can refine submissions
  • Batch operations: Efficient handling of multiple submissions
  • Escalation system: Complex cases forwarded to administrators

5. Photo and Media Management

Photo System Features:

  • Multi-format support: JPEG, PNG with automatic optimization
  • EXIF extraction: Automatic date/time capture from metadata
  • Approval workflow: Moderation for user-uploaded content
  • Smart storage: Organized directory structure by content type
  • Primary photo designation: Featured image selection per entity

Upload Interface:

<!-- Modal photo upload -->
<div x-data="photoUploadModal()" 
     @show-photo-upload.window="show = true">
  <form hx-post="{% url 'media:upload' %}" 
        hx-encoding="multipart/form-data">
    {% csrf_token %}
    <input type="file" multiple accept="image/*">
    <input type="text" placeholder="Caption (optional)">
  </form>
</div>

6. User Authentication and Profiles

Authentication Features:

  • Social OAuth: Google and Discord integration
  • Custom profiles: Display names, avatars, bio information
  • Role-based permissions: User, Moderator, Admin, Superuser levels
  • Theme preferences: User-specific dark/light mode settings

Profile Statistics:

<!-- User profile stats -->
<div class="profile-stats grid-stats">
  <div class="stat-card">
    <span class="stat-value">{{ profile.coaster_credits }}</span>
    <span class="stat-label">Coaster Credits</span>
  </div>
  <div class="stat-card">
    <span class="stat-value">{{ profile.dark_ride_credits }}</span>
    <span class="stat-label">Dark Rides</span>
  </div>
  <!-- Additional stat types... -->
</div>

7. History and Audit System

Change Tracking Features:

  • Complete audit trails: Every modification recorded
  • Diff visualization: Before/after comparisons
  • User attribution: Change tracking by user
  • Revert capability: Rollback to previous versions
  • Performance monitoring: Query and response time tracking

Accessibility and Responsive Design

Mobile-First Approach

  • Responsive breakpoints: 540px, 768px, 1024px, 1280px+
  • Touch-friendly interfaces: Appropriate button sizes and spacing
  • Optimized content hierarchy: Essential information prioritized on small screens

Accessibility Features

  • Semantic HTML: Proper heading structure and landmarks
  • ARIA labels: Screen reader support for interactive elements
  • Keyboard navigation: Full keyboard accessibility
  • Color contrast: WCAG AA compliant color schemes
  • Focus indicators: Clear focus states for interactive elements

Page Structure and Templates

Template Hierarchy and Organization

Base Template Architecture

<!-- templates/base/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="csrf-token" content="{{ csrf_token }}">
  <title>{% block title %}ThrillWiki{% endblock %}</title>

  <!-- Theme prevention script (prevents flash) -->
  <script>
    let theme = localStorage.getItem("theme");
    if (!theme) {
      theme = window.matchMedia("(prefers-color-scheme: dark)").matches
        ? "dark" : "light";
      localStorage.setItem("theme", theme);
    }
    if (theme === "dark") {
      document.documentElement.classList.add("dark");
    }
  </script>

  <!-- External dependencies -->
  <script src="https://unpkg.com/htmx.org@1.9.6"></script>
  <script defer src="{% static 'js/alpine.min.js' %}"></script>
  
  <!-- Styling -->
  <link href="{% static 'css/tailwind.css' %}" rel="stylesheet">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">

  {% block extra_head %}{% endblock %}
</head>
<body class="flex flex-col min-h-screen text-gray-900 
             bg-gradient-to-br from-white via-blue-50 to-indigo-50 
             dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950 
             dark:text-white">
  
  {% include "base/header.html" %}
  
  {% include "base/flash_messages.html" %}
  
  <main class="container flex-grow px-6 py-8 mx-auto">
    {% block content %}{% endblock %}
  </main>
  
  {% include "base/footer.html" %}
  
  <script src="{% static 'js/main.js' %}"></script>
  {% block extra_js %}{% endblock %}
</body>
</html>

Component-Based Template System

Navigation Component (templates/base/header.html)
<header class="sticky top-0 z-40 border-b shadow-lg 
               bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg 
               border-gray-200/50 dark:border-gray-700/50">
  <nav class="container mx-auto nav-container">
    <div class="flex items-center justify-between">
      
      <!-- Logo -->
      <div class="flex items-center">
        <a href="{% url 'home' %}" 
           class="font-bold text-transparent transition-transform site-logo 
                  bg-gradient-to-r from-primary to-secondary bg-clip-text 
                  hover:scale-105">
          ThrillWiki
        </a>
      </div>

      <!-- Main Navigation Links -->
      <div class="flex items-center space-x-2 sm:space-x-4">
        <a href="{% url 'parks:park_list' %}" class="nav-link">
          <i class="fas fa-map-marker-alt"></i>
          <span>Parks</span>
        </a>
        <a href="{% url 'rides:global_ride_list' %}" class="nav-link">
          <i class="fas fa-rocket"></i>
          <span>Rides</span>
        </a>
      </div>

      <!-- Search Bar (Desktop) -->
      <div class="flex-1 hidden max-w-md mx-8 lg:flex">
        <form action="{% url 'search:search' %}" method="get" class="w-full">
          <div class="relative">
            <input type="text" name="q" 
                   placeholder="Search parks and rides..."
                   class="form-input">
          </div>
        </form>
      </div>

      <!-- User Menu and Theme Toggle -->
      <div class="flex items-center space-x-2 sm:space-x-6">
        {% include "base/theme_toggle.html" %}
        {% include "base/user_menu.html" %}
        {% include "base/mobile_menu_button.html" %}
      </div>
    </div>

    <!-- Mobile Menu -->
    {% include "base/mobile_menu.html" %}
  </nav>
</header>
Theme Toggle Component
<!-- templates/base/theme_toggle.html -->
<label for="theme-toggle" class="cursor-pointer">
  <input type="checkbox" id="theme-toggle" class="hidden">
  <div class="inline-flex items-center justify-center p-2 
              text-gray-500 transition-colors hover:text-primary 
              dark:text-gray-400 dark:hover:text-primary theme-toggle-btn"
       role="button" aria-label="Toggle dark mode">
    <i class="text-xl fas"></i>
  </div>
</label>

Page-Specific Template Patterns

Park Detail Page Structure

<!-- templates/parks/park_detail.html -->
{% extends "base/base.html" %}
{% load static park_tags %}

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

{% block extra_head %}
{% if park.location.exists %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
{% endif %}
{% endblock %}

{% block content %}
<div class="container px-4 mx-auto sm:px-6 lg:px-8">
  
  <!-- Dynamic Action Buttons -->
  <div hx-get="{% url 'parks:park_actions' park.slug %}"
       hx-trigger="load, auth-changed from:body"
       hx-swap="innerHTML">
  </div>

  <!-- Park Header -->
  <div class="p-compact mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
    <div class="text-center">
      <h1 class="text-3xl font-bold text-gray-900 dark:text-white lg:text-4xl">
        {{ park.name }}
      </h1>
      {% if park.formatted_location %}
      <div class="flex items-center justify-center mt-2 text-sm text-gray-600 dark:text-gray-400">
        <i class="mr-1 fas fa-map-marker-alt"></i>
        <p>{{ park.formatted_location }}</p>
      </div>
      {% endif %}
    </div>
  </div>

  <!-- Statistics Grid -->
  <div class="grid-stats mb-6">
    {% include "parks/partials/park_stats.html" %}
  </div>

  <!-- Photo Gallery -->
  {% if park.photos.exists %}
  <div class="p-optimized mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
    <h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2>
    {% include "media/partials/photo_display.html" %}
  </div>
  {% endif %}

  <!-- Main Content Grid -->
  <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
    
    <!-- Left Column: Description and Rides -->
    <div class="lg:col-span-2">
      {% include "parks/partials/park_description.html" %}
      {% include "parks/partials/park_rides.html" %}
    </div>

    <!-- Right Column: Map and Additional Info -->
    <div class="lg:col-span-1">
      {% include "parks/partials/park_map.html" %}
      {% include "parks/partials/park_history.html" %}
    </div>
  </div>
</div>

<!-- Photo Upload Modal -->
{% include "media/partials/photo_upload_modal.html" %}
{% endblock %}

{% block extra_js %}
<script src="{% static 'js/photo-gallery.js' %}"></script>
{% if park.location.exists %}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="{% static 'js/park-map.js' %}"></script>
{% endif %}
{% endblock %}

Reusable Partial Templates

Park Statistics Component
<!-- templates/parks/partials/park_stats.html -->
<!-- Operator - Priority Card (First Position) -->
{% if park.operator %}
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats card-stats-priority">
  <div class="text-center">
    <dt class="text-sm font-semibold text-gray-900 dark:text-white">Operator</dt>
    <dd class="mt-1">
      <a href="{% url 'operators:operator_detail' park.operator.slug %}"
         class="text-sm font-bold text-sky-900 dark:text-sky-400 hover:text-sky-800 dark:hover:text-sky-300">
        {{ park.operator.name }}
      </a>
    </dd>
  </div>
</div>
{% endif %}

<!-- Additional stats cards... -->
<a href="{% url 'parks:rides:ride_list' park.slug %}"
   class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats transition-transform hover:scale-[1.02]">
  <div class="text-center">
    <dt class="text-sm font-semibold text-gray-900 dark:text-white">Total Rides</dt>
    <dd class="mt-1 text-2xl font-bold text-sky-900 dark:text-sky-400">
      {{ park.ride_count|default:"N/A" }}
    </dd>
  </div>
</a>
Photo Display Component
<!-- templates/media/partials/photo_display.html -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3" 
     x-data="photoGallery()">
  {% for photo in photos %}
  <div class="relative group">
    <img src="{{ photo.image.url }}" 
         alt="{{ photo.alt_text|default:photo.caption }}"
         class="w-full h-48 object-cover rounded-lg shadow-md 
                transition-transform group-hover:scale-105 cursor-pointer"
         @click="openModal('{{ photo.image.url }}', '{{ photo.caption }}')">
    
    {% if photo.caption %}
    <div class="absolute bottom-0 left-0 right-0 p-2 
                bg-gradient-to-t from-black/70 to-transparent rounded-b-lg">
      <p class="text-white text-sm">{{ photo.caption }}</p>
    </div>
    {% endif %}
  </div>
  {% endfor %}
</div>

Form Templates and User Input

Dynamic Form Rendering

<!-- templates/parks/park_form.html -->
{% extends "base/base.html" %}

{% block content %}
<div class="max-w-4xl mx-auto">
  <div class="auth-card">
    <h1 class="auth-title">
      {% if is_edit %}Edit Park{% else %}Add New Park{% endif %}
    </h1>
    
    <form method="post" enctype="multipart/form-data" 
          class="space-y-6" 
          hx-post="{% if is_edit %}{% url 'parks:park_edit' object.slug %}{% else %}{% url 'parks:park_create' %}{% endif %}"
          hx-target="#form-container"
          hx-swap="innerHTML">
      {% csrf_token %}
      
      <!-- Dynamic field rendering -->
      {% for field in form %}
      <div class="form-group">
        <label for="{{ field.id_for_label }}" class="form-label">
          {{ field.label }}
          {% if field.field.required %}<span class="text-red-500">*</span>{% endif %}
        </label>
        {{ field|add_class:"form-input" }}
        {% if field.help_text %}
        <div class="form-hint">{{ field.help_text }}</div>
        {% endif %}
        {% if field.errors %}
        <div class="form-error">{{ field.errors }}</div>
        {% endif %}
      </div>
      {% endfor %}
      
      <div class="flex justify-end space-x-4">
        <a href="{% if is_edit %}{{ object.get_absolute_url }}{% else %}{% url 'parks:park_list' %}{% endif %}" 
           class="btn-secondary">Cancel</a>
        <button type="submit" class="btn-primary">
          {% if is_edit %}Update Park{% else %}Add Park{% endif %}
        </button>
      </div>
    </form>
  </div>
</div>
{% endblock %}

Static File Organization

Asset Structure

static/
├── css/
│   ├── src/
│   │   └── input.css          # Tailwind source
│   ├── tailwind.css           # Compiled Tailwind
│   └── alerts.css             # Custom alert styles
├── js/
│   ├── main.js                # Core functionality
│   ├── alerts.js              # Alert management
│   ├── photo-gallery.js       # Photo interactions
│   ├── park-map.js            # Map functionality
│   ├── location-autocomplete.js # Geographic search
│   └── alpine.min.js          # Alpine.js framework
└── images/
    ├── placeholders/          # Default images
    └── icons/                 # Custom icons

Template Tag Usage

Custom template tags enhance template functionality:

<!-- Using custom park tags -->
{% load park_tags %}

<!-- Status badge with automatic styling -->
<span class="{% park_status_class park.status %}">
  {{ park.get_status_display }}
</span>

<!-- Rating display with stars -->
{% if park.average_rating %}
{% rating_stars park.average_rating %}
{% endif %}

Services and Business Logic

Unified Map Service Architecture

The UnifiedMapService provides the core geographic functionality for ThrillWiki, handling location data for parks, rides, and companies through a sophisticated service layer.

Service Architecture Overview

UnifiedMapService
├── LocationAbstractionLayer     # Data source abstraction
├── ClusteringService           # Point clustering for performance
├── MapCacheService            # Intelligent caching
└── Data Structures            # Type-safe data containers

Core Service Implementation

# core/services/map_service.py
class UnifiedMapService:
    """
    Main service orchestrating map data retrieval, filtering, clustering, and caching.
    Provides a unified interface for all location types with performance optimization.
    """
    
    # Performance thresholds
    MAX_UNCLUSTERED_POINTS = 500
    MAX_CLUSTERED_POINTS = 2000
    DEFAULT_ZOOM_LEVEL = 10
    
    def __init__(self):
        self.location_layer = LocationAbstractionLayer()
        self.clustering_service = ClusteringService()
        self.cache_service = MapCacheService()
    
    def get_map_data(
        self,
        bounds: Optional[GeoBounds] = None,
        filters: Optional[MapFilters] = None,
        zoom_level: int = DEFAULT_ZOOM_LEVEL,
        cluster: bool = True,
        use_cache: bool = True
    ) -> MapResponse:
        """
        Primary method for retrieving unified map data with intelligent
        caching, clustering, and performance optimization.
        """
        # Implementation handles cache checking, database queries,
        # smart limiting, clustering decisions, and response caching
        pass
    
    def get_location_details(self, location_type: str, location_id: int) -> Optional[UnifiedLocation]:
        """Get detailed information for a specific location with caching."""
        pass
    
    def search_locations(
        self,
        query: str,
        bounds: Optional[GeoBounds] = None,
        location_types: Optional[Set[LocationType]] = None,
        limit: int = 50
    ) -> List[UnifiedLocation]:
        """Search locations with text query and geographic bounds."""
        pass

Data Structure System

The service uses type-safe data structures for all map operations:

# core/services/data_structures.py

class LocationType(Enum):
    """Types of locations supported by the map service."""
    PARK = "park"
    RIDE = "ride"
    COMPANY = "company"
    GENERIC = "generic"

@dataclass
class GeoBounds:
    """Geographic boundary box for spatial queries."""
    north: float
    south: float
    east: float
    west: float
    
    def to_polygon(self) -> Polygon:
        """Convert bounds to PostGIS Polygon for database queries."""
        return Polygon.from_bbox((self.west, self.south, self.east, self.north))
    
    def expand(self, factor: float = 1.1) -> 'GeoBounds':
        """Expand bounds by factor for buffer queries."""
        pass

@dataclass
class MapFilters:
    """Filtering options for map queries."""
    location_types: Optional[Set[LocationType]] = None
    park_status: Optional[Set[str]] = None
    ride_types: Optional[Set[str]] = None
    search_query: Optional[str] = None
    min_rating: Optional[float] = None
    has_coordinates: bool = True
    country: Optional[str] = None
    state: Optional[str] = None
    city: Optional[str] = None

@dataclass
class UnifiedLocation:
    """Standardized location representation across all entity types."""
    id: int
    location_type: LocationType
    name: str
    coordinates: Point
    url: str
    additional_data: Dict[str, Any] = field(default_factory=dict)

Moderation System

ThrillWiki implements a comprehensive moderation system for user-generated content edits:

Edit Submission Workflow

# moderation/models.py
@pghistory.track()
class EditSubmission(TrackedModel):
    """Tracks all proposed changes to parks and rides."""
    
    STATUS_CHOICES = [
        ("PENDING", "Pending"),
        ("APPROVED", "Approved"),
        ("REJECTED", "Rejected"),
        ("ESCALATED", "Escalated"),
    ]
    
    SUBMISSION_TYPE_CHOICES = [
        ("EDIT", "Edit Existing"),
        ("CREATE", "Create New"),
    ]
    
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField(null=True, blank=True)
    content_object = GenericForeignKey("content_type", "object_id")
    
    submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE_CHOICES)
    proposed_changes = models.JSONField()  # Stores field-level changes
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
    
    # Moderation tracking
    reviewed_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True, blank=True,
        related_name="reviewed_submissions"
    )
    reviewed_at = models.DateTimeField(null=True, blank=True)
    reviewer_notes = models.TextField(blank=True)

Moderation Features

  • Change Tracking: Every edit submission tracked with django-pghistory
  • Field-Level Changes: JSON storage of specific field modifications
  • Review Workflow: Pending → Approved/Rejected/Escalated states
  • Reviewer Assignment: Track who reviewed each submission
  • Audit Trail: Complete history of all moderation decisions

Search and Autocomplete

# Autocomplete integration for geographic search
class LocationAutocompleteView(autocomplete.Select2QuerySetView):
    """AJAX autocomplete for geographic locations."""
    
    def get_queryset(self):
        if not self.request.user.is_authenticated:
            return Location.objects.none()
        
        qs = Location.objects.filter(is_active=True)
        
        if self.q:
            qs = qs.filter(
                Q(name__icontains=self.q) |
                Q(city__icontains=self.q) |
                Q(state__icontains=self.q) |
                Q(country__icontains=self.q)
            )
        
        return qs.select_related('country', 'state').order_by('name')[:20]

Search Integration

  • HTMX-Powered Search: Real-time search suggestions without page reloads
  • Geographic Filtering: Search within specific bounds or regions
  • Multi-Model Search: Unified search across parks, rides, and companies
  • Performance Optimized: Cached results and smart query limiting

Development Workflow

Required Development Environment

UV Package Manager Integration

ThrillWiki exclusively uses UV for all Python package management and Django commands:

# CRITICAL: Always use these exact commands

# Package Installation
uv add <package>                    # Add new dependencies
uv add --dev <package>              # Add development dependencies

# Django Management Commands
uv run manage.py makemigrations     # Create migrations
uv run manage.py migrate            # Apply migrations
uv run manage.py createsuperuser    # Create admin user
uv run manage.py shell              # Start Django shell
uv run manage.py collectstatic      # Collect static files

# Development Server (CRITICAL - use exactly as shown)
lsof -ti :8000 | xargs kill -9; find . -type d -name "__pycache__" -exec rm -r {} +; uv run manage.py tailwind runserver

IMPORTANT: Never use python manage.py or pip install. The project is configured exclusively for UV.

Local Development Setup

# Initial setup
git clone <repository-url>
cd thrillwiki_django_no_react

# Install dependencies
uv sync

# Database setup (requires PostgreSQL with PostGIS)
uv run manage.py migrate

# Create superuser
uv run manage.py createsuperuser

# Install Tailwind CSS and build
uv run manage.py tailwind install
uv run manage.py tailwind build

# Start development server
lsof -ti :8000 | xargs kill -9; find . -type d -name "__pycache__" -exec rm -r {} +; uv run manage.py tailwind runserver

Database Requirements

PostgreSQL with PostGIS

-- Required PostgreSQL extensions
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;
CREATE EXTENSION IF NOT EXISTS postgis_raster;

Geographic Data

  • Coordinate System: WGS84 (SRID: 4326) for all geographic data
  • Point Storage: All locations stored as PostGIS Point geometry
  • Spatial Queries: Optimized with GiST indexes for geographic searches
  • Distance Calculations: Native PostGIS distance functions

Frontend Development

Tailwind CSS Workflow

# Development mode (watches for changes)
uv run manage.py tailwind start

# Production build
uv run manage.py tailwind build

# Custom CSS location
static/css/src/input.css    # Source file
static/css/tailwind.css     # Compiled output

JavaScript Integration

  • Alpine.js: Reactive components and state management
  • HTMX: AJAX interactions and partial page updates
  • Custom Scripts: Modular JavaScript in static/js/ directory

API Endpoints and URL Structure

Primary URL Configuration

Main Application Routes

# thrillwiki/urls.py
urlpatterns = [
    path("admin/", admin.site.urls),
    path("", HomeView.as_view(), name="home"),
    
    # Autocomplete URLs (must be before other URLs)
    path("ac/", autocomplete_urls),
    
    # Core functionality
    path("parks/", include("parks.urls", namespace="parks")),
    path("rides/", include("rides.urls", namespace="rides")),
    path("photos/", include("media.urls", namespace="photos")),
    
    # Search and API
    path("search/", include("core.urls.search", namespace="search")),
    path("api/map/", include("core.urls.map_urls", namespace="map_api")),
    
    # User management
    path("accounts/", include("accounts.urls")),
    path("accounts/", include("allauth.urls")),
    path("user/<str:username>/", ProfileView.as_view(), name="user_profile"),
    path("settings/", SettingsView.as_view(), name="settings"),
    
    # Moderation system
    path("moderation/", include("moderation.urls", namespace="moderation")),
    
    # Static pages
    path("terms/", TemplateView.as_view(template_name="pages/terms.html"), name="terms"),
    path("privacy/", TemplateView.as_view(template_name="pages/privacy.html"), name="privacy"),
]

Parks URL Structure

# parks/urls.py
app_name = "parks"

urlpatterns = [
    # Main park views
    path("", ParkSearchView.as_view(), name="park_list"),
    path("create/", ParkCreateView.as_view(), name="park_create"),
    path("<slug:slug>/", ParkDetailView.as_view(), name="park_detail"),
    path("<slug:slug>/edit/", ParkUpdateView.as_view(), name="park_update"),
    
    # HTMX endpoints
    path("add-park-button/", add_park_button, name="add_park_button"),
    path("search/location/", location_search, name="location_search"),
    path("search/reverse-geocode/", reverse_geocode, name="reverse_geocode"),
    path("areas/", get_park_areas, name="get_park_areas"),
    path("suggest_parks/", suggest_parks, name="suggest_parks"),
    
    # Park areas
    path("<slug:park_slug>/areas/<slug:area_slug>/", ParkAreaDetailView.as_view(), name="area_detail"),
    
    # Category-specific rides within parks
    path("<slug:park_slug>/roller_coasters/", ParkSingleCategoryListView.as_view(),
         {'category': 'RC'}, name="park_roller_coasters"),
    path("<slug:park_slug>/dark_rides/", ParkSingleCategoryListView.as_view(),
         {'category': 'DR'}, name="park_dark_rides"),
    path("<slug:park_slug>/flat_rides/", ParkSingleCategoryListView.as_view(),
         {'category': 'FR'}, name="park_flat_rides"),
    path("<slug:park_slug>/water_rides/", ParkSingleCategoryListView.as_view(),
         {'category': 'WR'}, name="park_water_rides"),
    path("<slug:park_slug>/transports/", ParkSingleCategoryListView.as_view(),
         {'category': 'TR'}, name="park_transports"),
    path("<slug:park_slug>/others/", ParkSingleCategoryListView.as_view(),
         {'category': 'OT'}, name="park_others"),
    
    # Nested rides URLs
    path("<slug:park_slug>/rides/", include("rides.park_urls", namespace="rides")),
]

API Endpoints

Map API

GET /api/map/data/
- bounds: Geographic bounds (north,south,east,west)
- zoom: Map zoom level
- filters: JSON-encoded filter parameters
- Returns: Unified location data with clustering

GET /api/map/location/<type>/<id>/
- Returns: Detailed location information

POST /api/map/search/
- query: Search text
- bounds: Optional geographic bounds
- types: Location types to search
- Returns: Matching locations

Search API

GET /search/
- q: Search query
- type: Entity type (park, ride, company)
- location: Geographic filter
- Returns: Search results with pagination

GET /search/suggest/
- q: Partial query for autocomplete
- Returns: Quick suggestions

HTMX Endpoints

All HTMX endpoints return HTML fragments for seamless page updates:

POST /parks/suggest_parks/          # Park suggestions for autocomplete
GET /parks/areas/                   # Dynamic area loading
POST /parks/search/location/        # Location search with coordinates
POST /parks/search/reverse-geocode/ # Address lookup from coordinates

Conclusion

ThrillWiki represents a sophisticated Django application implementing modern web development practices with a focus on performance, user experience, and maintainability. The project successfully combines:

Technical Excellence

  • Modern Django Patterns: Service-oriented architecture with clear separation of concerns
  • Geographic Capabilities: Full PostGIS integration for spatial data and mapping
  • Performance Optimization: Intelligent caching, query optimization, and clustering
  • Type Safety: Comprehensive use of dataclasses and enums for data integrity

User Experience

  • Responsive Design: Mobile-first approach with Tailwind CSS
  • Progressive Enhancement: HTMX for seamless interactions without JavaScript complexity
  • Dark Mode Support: Complete theming system with user preferences
  • Accessibility: WCAG-compliant components and navigation

Development Workflow

  • UV Integration: Modern Python package management with reproducible builds
  • Comprehensive Testing: Model validation, service testing, and frontend integration
  • Documentation: Extensive inline documentation and architectural decisions
  • Moderation System: Complete workflow for user-generated content management

Architectural Strengths

  1. Scalability: Service layer architecture supports growth and feature expansion
  2. Maintainability: Clear code organization with consistent patterns
  3. Performance: Optimized database queries and intelligent caching strategies
  4. Security: Authentication, authorization, and input validation throughout
  5. Extensibility: Plugin-ready architecture for additional features

The project demonstrates enterprise-level Django development practices while maintaining simplicity and developer experience. The combination of modern frontend techniques (HTMX, Alpine.js, Tailwind) with robust backend services creates a powerful platform for theme park and ride enthusiasts.

This documentation serves as both a technical reference and architectural guide for understanding and extending the ThrillWiki platform.