mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 16:11:08 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
421
backend/templates/README.md
Normal file
421
backend/templates/README.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# ThrillWiki Template System
|
||||
|
||||
This document describes the template architecture, conventions, and best practices for ThrillWiki.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
templates/
|
||||
├── base/ # Base templates
|
||||
│ └── base.html # Root template all pages extend
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── ui/ # UI primitives (button, card, toast)
|
||||
│ ├── modals/ # Modal components
|
||||
│ ├── pagination.html # Pagination (supports HTMX)
|
||||
│ ├── status_badge.html # Status badge (parks/rides)
|
||||
│ ├── stats_card.html # Statistics card
|
||||
│ └── history_panel.html # History/audit trail
|
||||
├── forms/ # Form-related templates
|
||||
│ ├── partials/ # Form field components
|
||||
│ │ ├── form_field.html
|
||||
│ │ ├── field_error.html
|
||||
│ │ └── field_success.html
|
||||
│ └── layouts/ # Form layout templates
|
||||
│ ├── stacked.html # Vertical layout
|
||||
│ ├── inline.html # Horizontal layout
|
||||
│ └── grid.html # Multi-column grid
|
||||
├── htmx/ # HTMX-specific templates
|
||||
│ ├── components/ # HTMX components
|
||||
│ └── README.md # HTMX documentation
|
||||
├── {app}/ # App-specific templates
|
||||
│ ├── {model}_list.html # List views
|
||||
│ ├── {model}_detail.html # Detail views
|
||||
│ ├── {model}_form.html # Form views
|
||||
│ └── partials/ # App-specific partials
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Template Inheritance
|
||||
|
||||
### Base Template
|
||||
|
||||
All pages extend `base/base.html`. Available blocks:
|
||||
|
||||
```django
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{# Page title (appears in <title> and meta tags) #}
|
||||
{% block title %}My Page - ThrillWiki{% endblock %}
|
||||
|
||||
{# Main page content #}
|
||||
{% block content %}
|
||||
<h1>Page Content</h1>
|
||||
{% endblock %}
|
||||
|
||||
{# Additional CSS/meta in <head> #}
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="...">
|
||||
{% endblock %}
|
||||
|
||||
{# Additional JavaScript before </body> #}
|
||||
{% block extra_js %}
|
||||
<script src="..."></script>
|
||||
{% endblock %}
|
||||
|
||||
{# Additional body classes #}
|
||||
{% block body_class %}custom-page{% endblock %}
|
||||
|
||||
{# Additional main element classes #}
|
||||
{% block main_class %}no-padding{% endblock %}
|
||||
|
||||
{# Override navigation (defaults to enhanced_header.html) #}
|
||||
{% block navigation %}{% endblock %}
|
||||
|
||||
{# Override footer #}
|
||||
{% block footer %}{% endblock %}
|
||||
|
||||
{# Meta tags for SEO #}
|
||||
{% block meta_description %}Page description{% endblock %}
|
||||
{% block og_title %}Open Graph title{% endblock %}
|
||||
{% block og_description %}Open Graph description{% endblock %}
|
||||
```
|
||||
|
||||
### Inheritance Example
|
||||
|
||||
```django
|
||||
{% extends "base/base.html" %}
|
||||
{% load static %}
|
||||
{% load park_tags %}
|
||||
|
||||
{% block title %}{{ park.name }} - ThrillWiki{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="{% static 'css/park-detail.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>{{ park.name }}</h1>
|
||||
{% include "parks/partials/park_header_badge.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'js/park-map.js' %}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
## Component Usage
|
||||
|
||||
### Pagination
|
||||
|
||||
```django
|
||||
{# Standard pagination #}
|
||||
{% include 'components/pagination.html' with page_obj=page_obj %}
|
||||
|
||||
{# HTMX-enabled pagination #}
|
||||
{% include 'components/pagination.html' with page_obj=page_obj use_htmx=True hx_target='#results' %}
|
||||
|
||||
{# Small size #}
|
||||
{% include 'components/pagination.html' with page_obj=page_obj size='sm' %}
|
||||
```
|
||||
|
||||
### Status Badge
|
||||
|
||||
```django
|
||||
{# Basic badge #}
|
||||
{% include 'components/status_badge.html' with status=park.status %}
|
||||
|
||||
{# Interactive badge with HTMX refresh #}
|
||||
{% include 'components/status_badge.html' with
|
||||
status=park.status
|
||||
badge_id='park-header-badge'
|
||||
refresh_trigger='park-status-changed'
|
||||
scroll_target='park-status-section'
|
||||
can_edit=perms.parks.change_park
|
||||
%}
|
||||
```
|
||||
|
||||
### Stats Card
|
||||
|
||||
```django
|
||||
{# Basic stat #}
|
||||
{% include 'components/stats_card.html' with label='Total Rides' value=park.ride_count %}
|
||||
|
||||
{# Clickable stat #}
|
||||
{% include 'components/stats_card.html' with label='Total Rides' value=42 link=rides_url %}
|
||||
|
||||
{# Priority stat (highlighted) #}
|
||||
{% include 'components/stats_card.html' with label='Operator' value=park.operator.name priority=True %}
|
||||
```
|
||||
|
||||
### History Panel
|
||||
|
||||
```django
|
||||
{# Basic history #}
|
||||
{% include 'components/history_panel.html' with history=history %}
|
||||
|
||||
{# With FSM toggle for moderators #}
|
||||
{% include 'components/history_panel.html' with
|
||||
history=history
|
||||
show_fsm_toggle=True
|
||||
fsm_history_url=fsm_url
|
||||
model_type='park'
|
||||
object_id=park.id
|
||||
can_view_fsm=perms.parks.change_park
|
||||
%}
|
||||
```
|
||||
|
||||
### Loading Indicator
|
||||
|
||||
```django
|
||||
{# Block indicator #}
|
||||
{% include 'htmx/components/loading_indicator.html' with id='loading' message='Loading...' %}
|
||||
|
||||
{# Inline indicator (in buttons) #}
|
||||
{% include 'htmx/components/loading_indicator.html' with id='btn-loading' inline=True size='sm' %}
|
||||
|
||||
{# Overlay indicator #}
|
||||
{% include 'htmx/components/loading_indicator.html' with id='overlay' mode='overlay' %}
|
||||
```
|
||||
|
||||
## Form Rendering
|
||||
|
||||
### Form Layouts
|
||||
|
||||
```django
|
||||
{# Stacked layout (default) #}
|
||||
{% include 'forms/layouts/stacked.html' with form=form %}
|
||||
|
||||
{# Inline/horizontal layout #}
|
||||
{% include 'forms/layouts/inline.html' with form=form %}
|
||||
|
||||
{# 2-column grid #}
|
||||
{% include 'forms/layouts/grid.html' with form=form cols=2 %}
|
||||
|
||||
{# With excluded fields #}
|
||||
{% include 'forms/layouts/stacked.html' with form=form exclude='password2' %}
|
||||
|
||||
{# Custom submit text #}
|
||||
{% include 'forms/layouts/stacked.html' with form=form submit_text='Save Changes' %}
|
||||
```
|
||||
|
||||
### Individual Fields
|
||||
|
||||
```django
|
||||
{# Standard field #}
|
||||
{% include 'forms/partials/form_field.html' with field=form.email %}
|
||||
|
||||
{# Field with custom label #}
|
||||
{% include 'forms/partials/form_field.html' with field=form.email label='Email Address' %}
|
||||
|
||||
{# Field with HTMX validation #}
|
||||
{% include 'forms/partials/form_field.html' with
|
||||
field=form.username
|
||||
hx_validate=True
|
||||
hx_validate_url='/api/validate/username/'
|
||||
%}
|
||||
```
|
||||
|
||||
## Template Tags
|
||||
|
||||
### Loading Order
|
||||
|
||||
Always load template tags in this order:
|
||||
|
||||
```django
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load park_tags %} {# App-specific #}
|
||||
{% load safe_html %} {# Sanitization #}
|
||||
{% load common_filters %} {# Utility filters #}
|
||||
{% load cache %} {# Caching (if used) #}
|
||||
```
|
||||
|
||||
### Available Filters
|
||||
|
||||
**common_filters:**
|
||||
```django
|
||||
{{ datetime|humanize_timedelta }} {# "2 hours ago" #}
|
||||
{{ text|truncate_smart:50 }} {# Truncate at word boundary #}
|
||||
{{ number|format_number }} {# "1,234,567" #}
|
||||
{{ number|format_compact }} {# "1.2K", "3.4M" #}
|
||||
{{ dict|get_item:"key" }} {# Safe dict access #}
|
||||
{{ count|pluralize_custom:"item,items" }}
|
||||
{{ field|add_class:"form-control" }}
|
||||
```
|
||||
|
||||
**safe_html:**
|
||||
```django
|
||||
{{ content|sanitize }} {# Full HTML sanitization #}
|
||||
{{ comment|sanitize_minimal }} {# Basic text only #}
|
||||
{{ text|strip_html }} {# Remove all HTML #}
|
||||
{{ data|json_safe }} {# Safe JSON for JS #}
|
||||
{% icon "check" class="w-4 h-4" %} {# SVG icon #}
|
||||
```
|
||||
|
||||
## Context Variables
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
| Type | Convention | Example |
|
||||
|------|------------|---------|
|
||||
| Single object | Lowercase model name | `park`, `ride`, `user` |
|
||||
| List/QuerySet | `{model}_list` | `park_list`, `ride_list` |
|
||||
| Paginated | `page_obj` | Django standard |
|
||||
| Single form | `form` | Standard |
|
||||
| Multiple forms | `{purpose}_form` | `login_form`, `signup_form` |
|
||||
|
||||
### Avoid
|
||||
|
||||
- Generic names: `object`, `item`, `obj`, `data`
|
||||
- Abbreviated names: `p`, `r`, `f`, `frm`
|
||||
|
||||
## HTMX Patterns
|
||||
|
||||
See `htmx/README.md` for detailed HTMX documentation.
|
||||
|
||||
### Quick Reference
|
||||
|
||||
```django
|
||||
{# Swap strategies #}
|
||||
hx-swap="innerHTML" {# Replace content inside #}
|
||||
hx-swap="outerHTML" {# Replace entire element #}
|
||||
hx-swap="beforeend" {# Append #}
|
||||
hx-swap="afterbegin" {# Prepend #}
|
||||
|
||||
{# Target naming #}
|
||||
hx-target="#park-123" {# Specific object #}
|
||||
hx-target="#results" {# Page section #}
|
||||
hx-target="this" {# Self #}
|
||||
|
||||
{# Event naming #}
|
||||
hx-trigger="park-status-changed from:body"
|
||||
hx-trigger="auth-changed from:body"
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
Use fragment caching for expensive template sections:
|
||||
|
||||
```django
|
||||
{% load cache %}
|
||||
|
||||
{# Cache navigation for 5 minutes per user #}
|
||||
{% cache 300 nav user.id %}
|
||||
{% include 'components/layout/enhanced_header.html' %}
|
||||
{% endcache %}
|
||||
|
||||
{# Cache stats with object version #}
|
||||
{% cache 600 park_stats park.id park.updated_at %}
|
||||
{% include 'parks/partials/park_stats.html' %}
|
||||
{% endcache %}
|
||||
```
|
||||
|
||||
### Cache Keys
|
||||
|
||||
Include in cache key:
|
||||
- User ID for personalized content
|
||||
- Object ID for object-specific content
|
||||
- `updated_at` for automatic invalidation
|
||||
|
||||
Do NOT cache:
|
||||
- User-specific actions (edit buttons)
|
||||
- Form CSRF tokens
|
||||
- Real-time data
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Single `<h1>` per page
|
||||
- [ ] Heading hierarchy (h1 → h2 → h3)
|
||||
- [ ] Labels for all form inputs
|
||||
- [ ] `aria-label` for icon-only buttons
|
||||
- [ ] `aria-describedby` for help text
|
||||
- [ ] `aria-invalid` for fields with errors
|
||||
- [ ] `role="alert"` for error messages
|
||||
- [ ] `aria-live` for dynamic content
|
||||
|
||||
### Skip Links
|
||||
|
||||
Base template includes skip link to main content:
|
||||
```html
|
||||
<a href="#main-content" class="sr-only focus:not-sr-only ...">
|
||||
Skip to main content
|
||||
</a>
|
||||
```
|
||||
|
||||
### Landmarks
|
||||
|
||||
```html
|
||||
<nav role="navigation" aria-label="Main navigation">
|
||||
<main role="main" aria-label="Main content">
|
||||
<footer role="contentinfo">
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Safe HTML Rendering
|
||||
|
||||
```django
|
||||
{# User content - ALWAYS sanitize #}
|
||||
{{ user_description|sanitize }}
|
||||
|
||||
{# Comments - minimal formatting #}
|
||||
{{ comment_text|sanitize_minimal }}
|
||||
|
||||
{# Remove all HTML #}
|
||||
{{ raw_text|strip_html }}
|
||||
|
||||
{# NEVER use |safe for user content #}
|
||||
{{ user_input|safe }} {# DANGEROUS! #}
|
||||
```
|
||||
|
||||
### JSON in Templates
|
||||
|
||||
```django
|
||||
{# Safe JSON for JavaScript #}
|
||||
<script>
|
||||
const data = {{ python_dict|json_safe }};
|
||||
</script>
|
||||
```
|
||||
|
||||
## Component Documentation Template
|
||||
|
||||
Each component should have a header comment:
|
||||
|
||||
```django
|
||||
{% comment %}
|
||||
Component Name
|
||||
==============
|
||||
|
||||
Brief description of what the component does.
|
||||
|
||||
Purpose:
|
||||
Detailed explanation of the component's purpose.
|
||||
|
||||
Usage Examples:
|
||||
{% include 'components/example.html' with param='value' %}
|
||||
|
||||
Parameters:
|
||||
- param1: Description (required/optional, default: value)
|
||||
- param2: Description
|
||||
|
||||
Dependencies:
|
||||
- Alpine.js for interactivity
|
||||
- HTMX for dynamic updates
|
||||
|
||||
Security: Notes about sanitization, XSS prevention
|
||||
{% endcomment %}
|
||||
```
|
||||
|
||||
## File Naming
|
||||
|
||||
| Type | Pattern | Example |
|
||||
|------|---------|---------|
|
||||
| List view | `{model}_list.html` | `park_list.html` |
|
||||
| Detail view | `{model}_detail.html` | `park_detail.html` |
|
||||
| Form view | `{model}_form.html` | `park_form.html` |
|
||||
| Partial | `{model}_{purpose}.html` | `park_header_badge.html` |
|
||||
| Component | `{purpose}.html` | `pagination.html` |
|
||||
Reference in New Issue
Block a user