mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 13:51:10 -05:00
511 lines
11 KiB
Markdown
511 lines
11 KiB
Markdown
# 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:
|
|
|
|
```html
|
|
<!-- 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:
|
|
|
|
```html
|
|
<!-- 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:
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
def park_list(request):
|
|
parks = Park.objects.all()
|
|
return render(request, 'parks/list.html', {'parks': parks})
|
|
```
|
|
|
|
#### HTMX-Aware View
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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
|
|
|
|
```html
|
|
<!-- 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
|
|
|
|
```html
|
|
<!-- 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
|
|
|
|
```html
|
|
<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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```html
|
|
<!-- 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```html
|
|
<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
|
|
|
|
```html
|
|
<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
|
|
|
|
```css
|
|
/* Use CSS variables */
|
|
.custom-element {
|
|
color: hsl(var(--foreground));
|
|
background: hsl(var(--background));
|
|
border-color: hsl(var(--border));
|
|
}
|
|
```
|
|
|
|
### 2. Use Component Classes
|
|
|
|
```html
|
|
<!-- 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
|
|
|
|
```html
|
|
<!-- 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
|
|
|
|
```html
|
|
<!-- 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```html
|
|
<!-- 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
|
|
|
|
```html
|
|
<div id="content"
|
|
hx-get="/parks/"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
{% include 'components/skeletons/card_grid_skeleton.html' %}
|
|
</div>
|
|
```
|
|
|
|
### 3. Lazy Load Content
|
|
|
|
```html
|
|
<!-- Load when visible -->
|
|
<div hx-get="/stats/"
|
|
hx-trigger="revealed"
|
|
hx-swap="innerHTML">
|
|
<div class="skeleton h-20"></div>
|
|
</div>
|
|
```
|
|
|
|
### 4. Debounce Input
|
|
|
|
```html
|
|
<!-- Debounce search input -->
|
|
<input hx-get="/search/"
|
|
hx-trigger="keyup changed delay:300ms"
|
|
hx-target="#results">
|
|
```
|
|
|
|
## Security Guidelines
|
|
|
|
### 1. CSRF Protection
|
|
|
|
```html
|
|
<!-- 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:
|
|
|
|
```python
|
|
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
|
|
|
|
```django
|
|
<!-- Auto-escaped by Django -->
|
|
{{ user_input }}
|
|
|
|
<!-- Explicitly mark as safe only for trusted content -->
|
|
{{ trusted_html|safe }}
|
|
```
|
|
|
|
### 4. Use Appropriate Permissions
|
|
|
|
```python
|
|
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
|
|
```
|