mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 14:51:08 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
273
backend/templates/htmx/README.md
Normal file
273
backend/templates/htmx/README.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# HTMX Templates and Patterns
|
||||
|
||||
This directory contains HTMX-specific templates and components for ThrillWiki.
|
||||
|
||||
## Overview
|
||||
|
||||
HTMX is used throughout ThrillWiki for dynamic content updates without full page reloads. This guide documents the standardized patterns and conventions.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
htmx/
|
||||
├── components/ # Reusable HTMX components
|
||||
│ ├── confirm_dialog.html # Confirmation modal
|
||||
│ ├── error_message.html # Error display
|
||||
│ ├── filter_badge.html # Filter tag/badge
|
||||
│ ├── inline_edit_field.html # Inline editing
|
||||
│ ├── loading_indicator.html # Loading spinners
|
||||
│ └── success_toast.html # Success notification
|
||||
├── partials/ # HTMX response partials
|
||||
└── README.md # This documentation
|
||||
```
|
||||
|
||||
## Swap Strategies
|
||||
|
||||
Use consistent swap strategies across the application:
|
||||
|
||||
| Strategy | Use Case | Example |
|
||||
|----------|----------|---------|
|
||||
| `innerHTML` | Replace content inside container | List updates, search results |
|
||||
| `outerHTML` | Replace entire element | Status badges, table rows |
|
||||
| `beforeend` | Append items | Infinite scroll, new items |
|
||||
| `afterbegin` | Prepend items | New items at top of list |
|
||||
|
||||
### Examples
|
||||
|
||||
```html
|
||||
<!-- Replace content inside container -->
|
||||
<div id="results"
|
||||
hx-get="/api/search"
|
||||
hx-swap="innerHTML">
|
||||
|
||||
<!-- Replace entire element (e.g., status badge) -->
|
||||
<span id="park-status"
|
||||
hx-get="/api/park/status"
|
||||
hx-swap="outerHTML">
|
||||
|
||||
<!-- Infinite scroll - append items -->
|
||||
<div id="item-list"
|
||||
hx-get="/api/items?page=2"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="beforeend">
|
||||
```
|
||||
|
||||
## Target Naming Conventions
|
||||
|
||||
Follow these naming patterns for `hx-target`:
|
||||
|
||||
| Pattern | Use Case | Example |
|
||||
|---------|----------|---------|
|
||||
| `#object-type-id` | Specific objects | `#park-123`, `#ride-456` |
|
||||
| `#section-name` | Page sections | `#results`, `#filters`, `#stats` |
|
||||
| `#modal-container` | Modal content | `#modal-container` |
|
||||
| `this` | Self-replacement | Status badges, inline edits |
|
||||
|
||||
### Examples
|
||||
|
||||
```html
|
||||
<!-- Target specific object -->
|
||||
<button hx-post="/api/parks/123/status"
|
||||
hx-target="#park-123"
|
||||
hx-swap="outerHTML">
|
||||
|
||||
<!-- Target page section -->
|
||||
<form hx-post="/api/search"
|
||||
hx-target="#results"
|
||||
hx-swap="innerHTML">
|
||||
|
||||
<!-- Self-replacement -->
|
||||
<span hx-get="/api/badge"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML">
|
||||
```
|
||||
|
||||
## Custom Event Naming
|
||||
|
||||
Use these conventions for custom HTMX events:
|
||||
|
||||
| Pattern | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `{model}-status-changed` | Status updates | `park-status-changed`, `ride-status-changed` |
|
||||
| `{model}-created` | New item created | `park-created`, `review-created` |
|
||||
| `{model}-updated` | Item updated | `ride-updated`, `photo-updated` |
|
||||
| `{model}-deleted` | Item deleted | `comment-deleted` |
|
||||
| `auth-changed` | Auth state change | User login/logout |
|
||||
|
||||
### Triggering Events
|
||||
|
||||
From Django views:
|
||||
```python
|
||||
response['HX-Trigger'] = 'park-status-changed'
|
||||
# or with data
|
||||
response['HX-Trigger'] = json.dumps({
|
||||
'showToast': {'type': 'success', 'message': 'Status updated!'}
|
||||
})
|
||||
```
|
||||
|
||||
Listening for events:
|
||||
```html
|
||||
<div hx-get="/api/park/header"
|
||||
hx-trigger="park-status-changed from:body">
|
||||
```
|
||||
|
||||
## Loading Indicators
|
||||
|
||||
Use the standardized loading indicator component:
|
||||
|
||||
```html
|
||||
<!-- Inline (in buttons) -->
|
||||
<button hx-post="/api/action" hx-indicator="#btn-loading">
|
||||
Save
|
||||
{% include 'htmx/components/loading_indicator.html' with id='btn-loading' inline=True size='sm' %}
|
||||
</button>
|
||||
|
||||
<!-- Block (below content) -->
|
||||
<div hx-get="/api/data" hx-indicator="#loading">
|
||||
Content
|
||||
</div>
|
||||
{% include 'htmx/components/loading_indicator.html' with id='loading' message='Loading...' %}
|
||||
|
||||
<!-- Overlay (covers container) -->
|
||||
<div class="relative" hx-get="/api/data" hx-indicator="#overlay">
|
||||
Content
|
||||
{% include 'htmx/components/loading_indicator.html' with id='overlay' mode='overlay' %}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
HTMX errors are handled globally in `base.html`. The system:
|
||||
|
||||
1. Shows toast notifications for different HTTP status codes
|
||||
2. Handles timeouts (30 second default)
|
||||
3. Handles network errors
|
||||
4. Supports retry logic
|
||||
|
||||
### Custom Error Responses
|
||||
|
||||
Return error templates for 4xx/5xx responses:
|
||||
```html
|
||||
{% include 'htmx/components/error_message.html' with title='Error' message='Something went wrong.' %}
|
||||
```
|
||||
|
||||
## Toast Notifications via HTMX
|
||||
|
||||
Trigger toast notifications from server responses:
|
||||
|
||||
```python
|
||||
from django.http import JsonResponse
|
||||
|
||||
def my_view(request):
|
||||
response = render(request, 'partial.html')
|
||||
response['HX-Trigger'] = json.dumps({
|
||||
'showToast': {
|
||||
'type': 'success', # success, error, warning, info
|
||||
'message': 'Action completed!',
|
||||
'duration': 5000 # optional, in milliseconds
|
||||
}
|
||||
})
|
||||
return response
|
||||
```
|
||||
|
||||
## Form Validation
|
||||
|
||||
Use inline HTMX validation for forms:
|
||||
|
||||
```html
|
||||
<input type="text"
|
||||
name="username"
|
||||
hx-post="/api/validate/username"
|
||||
hx-trigger="blur changed delay:500ms"
|
||||
hx-target="#username-feedback"
|
||||
hx-swap="innerHTML">
|
||||
<div id="username-feedback"></div>
|
||||
```
|
||||
|
||||
Validation endpoint returns:
|
||||
```html
|
||||
<!-- Success -->
|
||||
{% include 'forms/partials/field_success.html' with message='Username available' %}
|
||||
|
||||
<!-- Error -->
|
||||
{% include 'forms/partials/field_error.html' with errors=errors %}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Search with Debounce
|
||||
|
||||
```html
|
||||
<input type="search"
|
||||
hx-get="/api/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#results"
|
||||
hx-indicator="#search-loading">
|
||||
```
|
||||
|
||||
### Modal Content Loading
|
||||
|
||||
```html
|
||||
<button hx-get="/api/park/123/edit"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML"
|
||||
@click="$store.modal.open()">
|
||||
Edit
|
||||
</button>
|
||||
```
|
||||
|
||||
### Infinite Scroll
|
||||
|
||||
```html
|
||||
<div id="items">
|
||||
{% for item in items %}
|
||||
{% include 'item.html' %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<div hx-get="?page={{ page_obj.next_page_number }}"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#items > *">
|
||||
{% include 'htmx/components/loading_indicator.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Status Badge Refresh
|
||||
|
||||
```html
|
||||
<span id="park-header-badge"
|
||||
hx-get="{% url 'parks:park_header_badge' park.slug %}"
|
||||
hx-trigger="park-status-changed from:body"
|
||||
hx-swap="outerHTML">
|
||||
{% include 'components/status_badge.html' with status=park.status %}
|
||||
</span>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always specify `hx-swap`** even for default behavior (clarity)
|
||||
2. **Use meaningful target IDs** following naming conventions
|
||||
3. **Include loading indicators** for all async operations
|
||||
4. **Handle errors gracefully** with user-friendly messages
|
||||
5. **Debounce search/filter inputs** to reduce server load
|
||||
6. **Use `hx-push-url`** for URL changes that should be bookmarkable
|
||||
7. **Provide fallback** for JavaScript-disabled browsers where possible
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- CSRF tokens are automatically included via `hx-headers` in base.html
|
||||
- All HTMX endpoints should validate permissions
|
||||
- Use Django's `@require_http_methods` decorator
|
||||
- Sanitize any user input before rendering
|
||||
|
||||
## Debugging
|
||||
|
||||
Enable HTMX debugging in development:
|
||||
```javascript
|
||||
htmx.logAll();
|
||||
```
|
||||
|
||||
Check browser DevTools Network tab for HTMX requests (look for `HX-Request: true` header).
|
||||
191
backend/templates/htmx/components/README.md
Normal file
191
backend/templates/htmx/components/README.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# HTMX Components
|
||||
|
||||
This directory contains HTMX-related template components for loading states, error handling, and success feedback.
|
||||
|
||||
## Loading State Guidelines
|
||||
|
||||
### When to Use Each Type
|
||||
|
||||
| Scenario | Component | Example |
|
||||
|----------|-----------|---------|
|
||||
| Initial page load, full content replacement | Skeleton screens | Parks list loading |
|
||||
| Button actions, form submissions | Loading indicator (inline) | Submit button spinner |
|
||||
| Partial content updates | Loading indicator (block) | Search results loading |
|
||||
| Container replacement | Loading indicator (overlay) | Modal content loading |
|
||||
|
||||
### Components
|
||||
|
||||
#### loading_indicator.html
|
||||
|
||||
Standardized loading indicator for HTMX requests with three modes:
|
||||
|
||||
**Inline Mode** - For buttons and links:
|
||||
```django
|
||||
<button hx-post="/api/action" hx-indicator="#btn-loading">
|
||||
Save
|
||||
{% include 'htmx/components/loading_indicator.html' with id='btn-loading' mode='inline' %}
|
||||
</button>
|
||||
```
|
||||
|
||||
**Block Mode** (default) - For content areas:
|
||||
```django
|
||||
<div hx-get="/api/list" hx-indicator="#list-loading">
|
||||
<!-- Content -->
|
||||
</div>
|
||||
{% include 'htmx/components/loading_indicator.html' with id='list-loading' %}
|
||||
```
|
||||
|
||||
**Overlay Mode** - For containers:
|
||||
```django
|
||||
<div class="relative" hx-get="/api/data" hx-indicator="#overlay-loading">
|
||||
<!-- Content to cover -->
|
||||
{% include 'htmx/components/loading_indicator.html' with id='overlay-loading' mode='overlay' %}
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `id` | str | - | ID for hx-indicator targeting |
|
||||
| `message` | str | "Loading..." | Loading text to display |
|
||||
| `mode` | str | "block" | 'inline', 'block', or 'overlay' |
|
||||
| `size` | str | "md" | 'sm', 'md', or 'lg' |
|
||||
| `spinner` | str | "border" | 'spin' (fa-spinner) or 'border' (CSS) |
|
||||
|
||||
## Skeleton Screens
|
||||
|
||||
Located in `components/skeletons/`, these provide content-aware loading placeholders:
|
||||
|
||||
### Available Skeletons
|
||||
|
||||
| Component | Use Case |
|
||||
|-----------|----------|
|
||||
| `list_skeleton.html` | List views, search results |
|
||||
| `card_grid_skeleton.html` | Card-based grid layouts |
|
||||
| `detail_skeleton.html` | Detail/show pages |
|
||||
| `form_skeleton.html` | Form loading states |
|
||||
| `table_skeleton.html` | Data tables |
|
||||
|
||||
### Usage with HTMX
|
||||
|
||||
```django
|
||||
{# Initial content shows skeleton, replaced by HTMX #}
|
||||
<div id="parks-list"
|
||||
hx-get="/parks/"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
{% include 'components/skeletons/card_grid_skeleton.html' with cards=6 %}
|
||||
</div>
|
||||
```
|
||||
|
||||
### With hx-indicator
|
||||
|
||||
```django
|
||||
<div id="results">
|
||||
<!-- Results here -->
|
||||
</div>
|
||||
|
||||
{# Skeleton shown during loading #}
|
||||
<div id="results-skeleton" class="htmx-indicator">
|
||||
{% include 'components/skeletons/list_skeleton.html' %}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### error_message.html
|
||||
|
||||
Displays error messages with consistent styling:
|
||||
|
||||
```django
|
||||
{% include 'htmx/components/error_message.html' with
|
||||
message="Unable to load data"
|
||||
show_retry=True
|
||||
retry_url="/api/data"
|
||||
%}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `message` | str | - | Error message text |
|
||||
| `code` | str | - | HTTP status code |
|
||||
| `show_retry` | bool | False | Show retry button |
|
||||
| `retry_url` | str | - | URL for retry action |
|
||||
| `show_report` | bool | False | Show "Report Issue" link |
|
||||
|
||||
## Success Feedback
|
||||
|
||||
### success_toast.html
|
||||
|
||||
Triggers a toast notification via HTMX response:
|
||||
|
||||
```django
|
||||
{% include 'htmx/components/success_toast.html' with
|
||||
message="Park saved successfully"
|
||||
type="success"
|
||||
%}
|
||||
```
|
||||
|
||||
### Using HX-Trigger Header
|
||||
|
||||
From views, trigger toasts via response headers:
|
||||
|
||||
```python
|
||||
from django.http import HttpResponse
|
||||
|
||||
def save_park(request):
|
||||
# ... save logic ...
|
||||
response = HttpResponse()
|
||||
response['HX-Trigger'] = json.dumps({
|
||||
'showToast': {
|
||||
'type': 'success',
|
||||
'message': 'Park saved successfully'
|
||||
}
|
||||
})
|
||||
return response
|
||||
```
|
||||
|
||||
## HTMX Configuration
|
||||
|
||||
The base template configures HTMX with:
|
||||
|
||||
- 30-second timeout
|
||||
- Global view transitions enabled
|
||||
- Template fragments enabled
|
||||
- Comprehensive error handling
|
||||
|
||||
### Swap Strategies
|
||||
|
||||
| Strategy | Use Case |
|
||||
|----------|----------|
|
||||
| `innerHTML` | Replace content inside container (lists, search results) |
|
||||
| `outerHTML` | Replace entire element (status badges, individual items) |
|
||||
| `beforeend` | Append items (infinite scroll) |
|
||||
| `afterbegin` | Prepend items (new items at top) |
|
||||
|
||||
### Target Naming Conventions
|
||||
|
||||
- `#object-type-id` - For specific objects (e.g., `#park-123`)
|
||||
- `#section-name` - For page sections (e.g., `#results`, `#filters`)
|
||||
- `#modal-container` - For modals
|
||||
- `this` - For self-replacement
|
||||
|
||||
### Custom Events
|
||||
|
||||
- `{model}-status-changed` - Status updates (e.g., `park-status-changed`)
|
||||
- `auth-changed` - Authentication state changes
|
||||
- `{model}-created` - New item created
|
||||
- `{model}-updated` - Item updated
|
||||
- `{model}-deleted` - Item deleted
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always specify hx-indicator** for user feedback
|
||||
2. **Use skeleton screens** for initial page loads and full content replacement
|
||||
3. **Use inline indicators** for button actions
|
||||
4. **Use overlay indicators** for modal content loading
|
||||
5. **Add aria attributes** for accessibility (role="status", aria-busy)
|
||||
6. **Clean up loading states** after request completion (automatic with HTMX)
|
||||
@@ -1,33 +1,125 @@
|
||||
{% comment %}
|
||||
Loading Indicator Component
|
||||
HTMX Loading Indicator Component
|
||||
================================
|
||||
|
||||
Displays a loading spinner for HTMX requests.
|
||||
A standardized loading indicator for HTMX requests.
|
||||
|
||||
Optional context:
|
||||
- size: 'sm', 'md', or 'lg' (defaults to 'md')
|
||||
- inline: Whether to render inline (defaults to false)
|
||||
- message: Loading message text (defaults to 'Loading...')
|
||||
- id: Optional ID for the indicator element
|
||||
Purpose:
|
||||
Provides consistent loading feedback during HTMX requests.
|
||||
Can be used inline, block, or as an overlay.
|
||||
|
||||
Usage Examples:
|
||||
Inline spinner (in button/link):
|
||||
<button hx-get="/api/data" hx-indicator="#btn-loading">
|
||||
Load Data
|
||||
{% include 'htmx/components/loading_indicator.html' with id='btn-loading' inline=True size='sm' %}
|
||||
</button>
|
||||
|
||||
Block indicator (below content):
|
||||
<div hx-get="/api/list" hx-indicator="#list-loading">
|
||||
Content here
|
||||
</div>
|
||||
{% include 'htmx/components/loading_indicator.html' with id='list-loading' message='Loading items...' %}
|
||||
|
||||
Overlay indicator (covers container):
|
||||
<div class="relative" hx-get="/api/data" hx-indicator="#overlay-loading">
|
||||
Content to cover
|
||||
{% include 'htmx/components/loading_indicator.html' with id='overlay-loading' mode='overlay' %}
|
||||
</div>
|
||||
|
||||
Custom spinner:
|
||||
{% include 'htmx/components/loading_indicator.html' with id='custom-loading' spinner='border' %}
|
||||
|
||||
Parameters:
|
||||
Optional:
|
||||
- id: Unique identifier for the indicator (used with hx-indicator)
|
||||
- message: Loading text to display (default: 'Loading...')
|
||||
- mode: 'inline', 'block', or 'overlay' (default: 'block')
|
||||
- inline: Shortcut for mode='inline' (backwards compatible)
|
||||
- size: 'sm', 'md', 'lg' (default: 'md')
|
||||
- spinner: 'spin' (fa-spinner) or 'border' (CSS animation) (default: 'border')
|
||||
|
||||
Dependencies:
|
||||
- HTMX for .htmx-indicator class behavior
|
||||
- Tailwind CSS for styling
|
||||
- Font Awesome icons (for 'spin' spinner)
|
||||
|
||||
Accessibility:
|
||||
- Uses role="status" and aria-live="polite" for screen readers
|
||||
- aria-hidden while not loading (HTMX handles visibility)
|
||||
- Loading message announced to screen readers
|
||||
{% endcomment %}
|
||||
|
||||
{% if inline %}
|
||||
<!-- Inline Loading Indicator -->
|
||||
<span class="htmx-indicator inline-flex items-center gap-2 {% if size == 'sm' %}text-sm{% elif size == 'lg' %}text-lg{% endif %}"
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
aria-hidden="true">
|
||||
<i class="fas fa-spinner fa-spin text-blue-500"></i>
|
||||
{% if message %}<span class="text-gray-500 dark:text-gray-400">{{ message }}</span>{% endif %}
|
||||
</span>
|
||||
{% else %}
|
||||
<!-- Block Loading Indicator -->
|
||||
<div class="htmx-indicator flex items-center justify-center p-4 {% if size == 'sm' %}p-2{% elif size == 'lg' %}p-6{% endif %}"
|
||||
{# Support both 'inline' param and 'mode' param #}
|
||||
{% with actual_mode=mode|default:inline|yesno:'inline,block' %}
|
||||
|
||||
{% if actual_mode == 'overlay' %}
|
||||
{# ============================================
|
||||
Overlay Mode - Covers parent element
|
||||
Parent must have position: relative
|
||||
============================================ #}
|
||||
<div class="htmx-indicator absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm z-10 rounded-lg"
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
aria-hidden="true">
|
||||
role="status"
|
||||
aria-live="polite">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
{# Spinner #}
|
||||
{% if spinner == 'spin' %}
|
||||
<i class="fas fa-spinner fa-spin {% if size == 'sm' %}text-xl{% elif size == 'lg' %}text-5xl{% else %}text-3xl{% endif %} text-blue-500"
|
||||
aria-hidden="true"></i>
|
||||
{% else %}
|
||||
<div class="{% if size == 'sm' %}w-6 h-6 border-3{% elif size == 'lg' %}w-12 h-12 border-4{% else %}w-8 h-8 border-4{% endif %} border-blue-500 rounded-full animate-spin border-t-transparent"
|
||||
aria-hidden="true"></div>
|
||||
{% endif %}
|
||||
{# Message #}
|
||||
<span class="{% if size == 'sm' %}text-sm{% elif size == 'lg' %}text-lg{% else %}text-base{% endif %} font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ message|default:"Loading..." }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif actual_mode == 'inline' or inline %}
|
||||
{# ============================================
|
||||
Inline Mode - For use within buttons/links
|
||||
============================================ #}
|
||||
<span class="htmx-indicator inline-flex items-center gap-2"
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
role="status"
|
||||
aria-live="polite">
|
||||
{% if spinner == 'spin' or spinner == '' %}
|
||||
<i class="fas fa-spinner fa-spin {% if size == 'sm' %}text-sm{% elif size == 'lg' %}text-lg{% endif %} text-blue-500"
|
||||
aria-hidden="true"></i>
|
||||
{% else %}
|
||||
<div class="{% if size == 'sm' %}w-4 h-4 border-2{% elif size == 'lg' %}w-6 h-6 border-3{% else %}w-5 h-5 border-2{% endif %} border-current rounded-full animate-spin border-t-transparent"
|
||||
aria-hidden="true"></div>
|
||||
{% endif %}
|
||||
{% if message %}<span class="text-gray-500 dark:text-gray-400">{{ message }}</span>{% endif %}
|
||||
{% if not message %}<span class="sr-only">Loading...</span>{% endif %}
|
||||
</span>
|
||||
|
||||
{% else %}
|
||||
{# ============================================
|
||||
Block Mode (default) - Centered block
|
||||
============================================ #}
|
||||
<div class="htmx-indicator flex items-center justify-center {% if size == 'sm' %}p-2{% elif size == 'lg' %}p-6{% else %}p-4{% endif %}"
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
role="status"
|
||||
aria-live="polite">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="{% if size == 'sm' %}w-5 h-5{% elif size == 'lg' %}w-10 h-10{% else %}w-8 h-8{% endif %} border-4 border-blue-500 rounded-full animate-spin border-t-transparent"></div>
|
||||
{# Spinner #}
|
||||
{% if spinner == 'spin' %}
|
||||
<i class="fas fa-spinner fa-spin {% if size == 'sm' %}text-lg{% elif size == 'lg' %}text-3xl{% else %}text-2xl{% endif %} text-blue-500"
|
||||
aria-hidden="true"></i>
|
||||
{% else %}
|
||||
<div class="{% if size == 'sm' %}w-5 h-5 border-3{% elif size == 'lg' %}w-10 h-10 border-4{% else %}w-8 h-8 border-4{% endif %} border-blue-500 rounded-full animate-spin border-t-transparent"
|
||||
aria-hidden="true"></div>
|
||||
{% endif %}
|
||||
{# Message #}
|
||||
<span class="{% if size == 'sm' %}text-sm{% elif size == 'lg' %}text-lg{% else %}text-base{% endif %} text-gray-600 dark:text-gray-300">
|
||||
{{ message|default:"Loading..." }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<nav class="htmx-pagination" role="navigation" aria-label="Pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<button hx-get="{{ request.path }}?page={{ page_obj.previous_page_number }}" hx-swap="#results">Previous</button>
|
||||
{% endif %}
|
||||
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
{% if page_obj.has_next %}
|
||||
<button hx-get="{{ request.path }}?page={{ page_obj.next_page_number }}" hx-swap="#results">Next</button>
|
||||
{% endif %}
|
||||
</nav>
|
||||
Reference in New Issue
Block a user