- 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
55 KiB
ThrillWiki Project Documentation
Table of Contents
- Project Overview
- Technical Stack and Architecture
- Database Models and Relationships
- Visual Theme and Design System
- Frontend Implementation Patterns
- User Experience and Key Features
- Page Structure and Templates
- Services and Business Logic
- Development Workflow
- 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)
-
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
-
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
-
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
Location-Based Search
# 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
- Scalability: Service layer architecture supports growth and feature expansion
- Maintainability: Clear code organization with consistent patterns
- Performance: Optimized database queries and intelligent caching strategies
- Security: Authentication, authorization, and input validation throughout
- 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.