mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 11:51:09 -05:00
11 KiB
11 KiB
Developer Guidelines
Best practices for UX development in ThrillWiki.
General Principles
1. Progressive Enhancement
All features should work without JavaScript, with HTMX and Alpine.js enhancing the experience:
<!-- Form works with or without JS -->
<form method="post" action="/parks/create/"
hx-post="/parks/create/"
hx-target="#form-container">
{% csrf_token %}
<!-- Form fields -->
<button type="submit">Save</button>
</form>
2. Consistent Feedback
Every user action should provide immediate feedback:
- Loading states: Show progress during requests
- Success messages: Confirm completed actions
- Error messages: Explain what went wrong
- Validation: Inline feedback on form fields
3. Accessibility First
All components must be accessible:
- Proper ARIA attributes
- Keyboard navigation
- Screen reader support
- Focus management
- Sufficient color contrast
4. Mobile-First Design
Design for mobile first, then enhance for larger screens:
<!-- Mobile: stacked, Desktop: side-by-side -->
<div class="flex flex-col md:flex-row gap-4">
<div class="w-full md:w-1/2">Left</div>
<div class="w-full md:w-1/2">Right</div>
</div>
File Organization
Template Structure
templates/
├── base/
│ └── base.html # Base template
├── components/
│ ├── layout/ # Layout components
│ ├── modals/ # Modal components
│ ├── navigation/ # Navigation components
│ ├── skeletons/ # Skeleton screens
│ └── ui/ # UI primitives
├── forms/
│ ├── layouts/ # Form layouts
│ └── partials/ # Form field components
├── htmx/
│ └── components/ # HTMX-specific components
└── [app_name]/
├── list.html # Full page templates
├── list_partial.html # HTMX partials
└── _card.html # Reusable fragments
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Full page | name.html |
list.html |
| HTMX partial | name_partial.html |
list_partial.html |
| Fragment | _name.html |
_card.html |
| Component | name.html in components/ |
button.html |
Django Integration
Context Processors
Use context processors for global data:
# apps/core/context_processors.py
def breadcrumbs(request):
return {'breadcrumbs': getattr(request, 'breadcrumbs', [])}
def page_meta(request):
return {'page_meta': getattr(request, 'page_meta', {})}
View Patterns
Standard View
def park_list(request):
parks = Park.objects.all()
return render(request, 'parks/list.html', {'parks': parks})
HTMX-Aware View
from apps.core.htmx_utils import is_htmx_request
def park_list(request):
parks = Park.objects.all()
template = 'parks/list_partial.html' if is_htmx_request(request) else 'parks/list.html'
return render(request, template, {'parks': parks})
Using Decorator
from apps.core.htmx_utils import htmx_partial
@htmx_partial('parks/list.html')
def park_list(request):
parks = Park.objects.all()
return ({'parks': parks},)
Breadcrumbs
Set breadcrumbs in views:
from apps.core.utils import BreadcrumbBuilder
def park_detail(request, slug):
park = get_object_or_404(Park, slug=slug)
request.breadcrumbs = (
BreadcrumbBuilder()
.add_home()
.add('Parks', reverse('parks:list'))
.add_current(park.name)
.build()
)
return render(request, 'parks/detail.html', {'park': park})
Page Meta
Set meta information in views:
from apps.core.utils import build_meta_context
def park_detail(request, slug):
park = get_object_or_404(Park, slug=slug)
request.page_meta = build_meta_context(
title=park.name,
description=park.description,
instance=park,
request=request,
)
return render(request, 'parks/detail.html', {'park': park})
HTMX Best Practices
1. Use Appropriate HTTP Methods
<!-- GET for read operations -->
<button hx-get="/parks/">Load Parks</button>
<!-- POST for create -->
<form hx-post="/parks/create/">...</form>
<!-- PUT/PATCH for update -->
<form hx-put="/parks/123/edit/">...</form>
<!-- DELETE for remove -->
<button hx-delete="/parks/123/">Delete</button>
2. Target Specific Elements
<!-- Good: Specific target -->
<button hx-get="/parks/" hx-target="#parks-list">Load</button>
<!-- Avoid: Replacing large sections unnecessarily -->
<button hx-get="/parks/" hx-target="#main-content">Load</button>
3. Use Loading Indicators
<button hx-get="/parks/"
hx-target="#parks-list"
hx-indicator="#loading">
Load Parks
</button>
<div id="loading" class="htmx-indicator">
{% include 'htmx/components/loading_indicator.html' %}
</div>
4. Handle Errors Gracefully
def create_park(request):
form = ParkForm(request.POST)
if not form.is_valid():
# Return form with errors, status 422
return render(
request,
'parks/_form.html',
{'form': form},
status=422
)
park = form.save()
return htmx_success(f'{park.name} created!')
5. Use Response Headers
from apps.core.htmx_utils import (
htmx_success,
htmx_error,
htmx_redirect,
htmx_modal_close,
)
# Success with toast
return htmx_success('Park saved!')
# Error with toast
return htmx_error('Validation failed', status=422)
# Redirect
return htmx_redirect('/parks/')
# Close modal and refresh
return htmx_modal_close(
message='Saved!',
refresh_target='#parks-list'
)
Alpine.js Best Practices
1. Keep Components Small
<!-- Good: Focused component -->
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open">Content</div>
</div>
<!-- Avoid: Large monolithic components -->
<div x-data="{
open: false,
items: [],
selectedItem: null,
loading: false,
// ... many more properties
}">
2. Use Stores for Global State
// stores/index.js
Alpine.store('toast', {
messages: [],
success(message, duration = 5000) { /* ... */ },
error(message, duration = 0) { /* ... */ },
});
// Usage anywhere
Alpine.store('toast').success('Saved!');
3. Initialize from Server Data
<div x-data="{ items: {{ items_json|safe }} }">
<template x-for="item in items">
<div x-text="item.name"></div>
</template>
</div>
4. Use Magic Properties
<div x-data="{ message: '' }">
<!-- $refs for DOM access -->
<input x-ref="input" x-model="message">
<button @click="$refs.input.focus()">Focus</button>
<!-- $watch for reactive updates -->
<div x-init="$watch('message', value => console.log(value))">
<!-- $nextTick for DOM updates -->
<button @click="items.push('new'); $nextTick(() => scrollToBottom())">
Add
</button>
</div>
CSS/Tailwind Best Practices
1. Use Design Tokens
/* Use CSS variables */
.custom-element {
color: hsl(var(--foreground));
background: hsl(var(--background));
border-color: hsl(var(--border));
}
2. Use Component Classes
<!-- Good: Semantic classes -->
<button class="btn btn-primary btn-lg">Save</button>
<!-- Avoid: Long utility chains for common patterns -->
<button class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium">
Save
</button>
3. Responsive Design
<!-- Mobile-first -->
<div class="p-4 md:p-6 lg:p-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Items -->
</div>
</div>
4. Dark Mode Support
<!-- Automatic dark mode via CSS variables -->
<div class="bg-background text-foreground">
Content adapts to theme
</div>
<!-- Explicit dark mode variants -->
<div class="bg-white dark:bg-gray-800">
Explicit dark override
</div>
Testing Guidelines
1. Test User Flows
def test_create_park_flow(self):
# Navigate to create page
response = self.client.get('/parks/create/')
self.assertEqual(response.status_code, 200)
# Submit form
response = self.client.post('/parks/create/', {
'name': 'Test Park',
'location': 'Test Location',
})
# Verify redirect or success
self.assertEqual(response.status_code, 302)
self.assertTrue(Park.objects.filter(name='Test Park').exists())
2. Test HTMX Responses
def test_htmx_park_list(self):
response = self.client.get('/parks/', HTTP_HX_REQUEST='true')
# Should return partial template
self.assertTemplateUsed(response, 'parks/list_partial.html')
# Should not include base template chrome
self.assertNotContains(response, '<html')
3. Test Error States
def test_form_validation_errors(self):
response = self.client.post('/parks/create/', {
'name': '', # Required field
})
self.assertEqual(response.status_code, 422)
self.assertContains(response, 'This field is required')
Performance Guidelines
1. Minimize DOM Updates
<!-- Good: Update specific element -->
<button hx-get="/count/" hx-target="#count-badge">Refresh</button>
<!-- Avoid: Replacing entire page section -->
<button hx-get="/dashboard/" hx-target="#main">Refresh</button>
2. Use Skeleton Screens
<div id="content"
hx-get="/parks/"
hx-trigger="load"
hx-swap="innerHTML">
{% include 'components/skeletons/card_grid_skeleton.html' %}
</div>
3. Lazy Load Content
<!-- Load when visible -->
<div hx-get="/stats/"
hx-trigger="revealed"
hx-swap="innerHTML">
<div class="skeleton h-20"></div>
</div>
4. Debounce Input
<!-- Debounce search input -->
<input hx-get="/search/"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
Security Guidelines
1. CSRF Protection
<!-- Include token in body -->
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<!-- Or in forms -->
<form hx-post="/create/">
{% csrf_token %}
</form>
2. Validate on Server
Always validate data server-side, even with client-side validation:
def create_park(request):
form = ParkForm(request.POST)
if not form.is_valid():
return htmx_error('Invalid data', status=422)
# Process valid data
3. Escape User Content
<!-- Auto-escaped by Django -->
{{ user_input }}
<!-- Explicitly mark as safe only for trusted content -->
{{ trusted_html|safe }}
4. Use Appropriate Permissions
from django.contrib.auth.decorators import login_required, permission_required
@login_required
@permission_required('parks.add_park')
def create_park(request):
# Only authenticated users with permission
pass