mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 18:11:08 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
225
backend/templates/forms/README.md
Normal file
225
backend/templates/forms/README.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Form Templates
|
||||
|
||||
This directory contains form-related templates for ThrillWiki.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
forms/
|
||||
├── partials/ # Individual form components
|
||||
│ ├── form_field.html # Complete form field
|
||||
│ ├── field_error.html # Error messages
|
||||
│ ├── field_success.html # Success indicator
|
||||
│ └── form_actions.html # Submit/cancel buttons
|
||||
├── layouts/ # Form layout templates
|
||||
│ ├── stacked.html # Vertical layout
|
||||
│ ├── inline.html # Horizontal layout
|
||||
│ └── grid.html # Multi-column grid
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Form Layouts
|
||||
|
||||
### Stacked Layout (Default)
|
||||
|
||||
Vertical layout with full-width fields:
|
||||
|
||||
```django
|
||||
{% include 'forms/layouts/stacked.html' with form=form %}
|
||||
|
||||
{# With options #}
|
||||
{% include 'forms/layouts/stacked.html' with
|
||||
form=form
|
||||
submit_text='Save Changes'
|
||||
show_cancel=True
|
||||
cancel_url='/parks/'
|
||||
%}
|
||||
```
|
||||
|
||||
### Inline Layout
|
||||
|
||||
Horizontal layout with labels beside inputs:
|
||||
|
||||
```django
|
||||
{% include 'forms/layouts/inline.html' with form=form %}
|
||||
|
||||
{# Custom label width #}
|
||||
{% include 'forms/layouts/inline.html' with form=form label_width='w-1/4' %}
|
||||
```
|
||||
|
||||
### Grid Layout
|
||||
|
||||
Multi-column responsive grid:
|
||||
|
||||
```django
|
||||
{# 2-column grid #}
|
||||
{% include 'forms/layouts/grid.html' with form=form cols=2 %}
|
||||
|
||||
{# 3-column grid #}
|
||||
{% include 'forms/layouts/grid.html' with form=form cols=3 %}
|
||||
|
||||
{# Full-width description field #}
|
||||
{% include 'forms/layouts/grid.html' with form=form cols=2 full_width='description' %}
|
||||
```
|
||||
|
||||
## Layout Parameters
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `form` | Django form object | Required |
|
||||
| `exclude` | Comma-separated fields to exclude | None |
|
||||
| `fields` | Comma-separated fields to include | All |
|
||||
| `show_help` | Show help text | True |
|
||||
| `show_required` | Show required indicator | True |
|
||||
| `submit_text` | Submit button text | 'Submit' |
|
||||
| `submit_class` | Submit button CSS class | 'btn-primary' |
|
||||
| `show_cancel` | Show cancel button | False |
|
||||
| `cancel_url` | URL for cancel link | None |
|
||||
| `cancel_text` | Cancel button text | 'Cancel' |
|
||||
| `show_actions` | Show action buttons | True |
|
||||
|
||||
## Individual Components
|
||||
|
||||
### Form Field
|
||||
|
||||
Complete field with label, input, help, and errors:
|
||||
|
||||
```django
|
||||
{% include 'forms/partials/form_field.html' with field=form.email %}
|
||||
|
||||
{# Custom label #}
|
||||
{% include 'forms/partials/form_field.html' with field=form.email label='Your Email' %}
|
||||
|
||||
{# Without label #}
|
||||
{% include 'forms/partials/form_field.html' with field=form.search show_label=False %}
|
||||
|
||||
{# Inline layout #}
|
||||
{% include 'forms/partials/form_field.html' with field=form.email layout='inline' %}
|
||||
|
||||
{# HTMX validation #}
|
||||
{% include 'forms/partials/form_field.html' with
|
||||
field=form.username
|
||||
hx_validate=True
|
||||
hx_validate_url='/api/validate/username/'
|
||||
%}
|
||||
```
|
||||
|
||||
### Field Error
|
||||
|
||||
Error message display:
|
||||
|
||||
```django
|
||||
{% include 'forms/partials/field_error.html' with errors=field.errors %}
|
||||
|
||||
{# Without icon #}
|
||||
{% include 'forms/partials/field_error.html' with errors=field.errors show_icon=False %}
|
||||
|
||||
{# Different size #}
|
||||
{% include 'forms/partials/field_error.html' with errors=field.errors size='md' %}
|
||||
```
|
||||
|
||||
### Field Success
|
||||
|
||||
Success indicator for validation:
|
||||
|
||||
```django
|
||||
{% include 'forms/partials/field_success.html' with message='Username available' %}
|
||||
|
||||
{# Just checkmark #}
|
||||
{% include 'forms/partials/field_success.html' %}
|
||||
```
|
||||
|
||||
### Form Actions
|
||||
|
||||
Submit and cancel buttons:
|
||||
|
||||
```django
|
||||
{% include 'forms/partials/form_actions.html' %}
|
||||
|
||||
{# With cancel #}
|
||||
{% include 'forms/partials/form_actions.html' with show_cancel=True cancel_url='/list/' %}
|
||||
|
||||
{# With loading state #}
|
||||
{% include 'forms/partials/form_actions.html' with show_loading=True %}
|
||||
|
||||
{# Left-aligned #}
|
||||
{% include 'forms/partials/form_actions.html' with align='left' %}
|
||||
|
||||
{# Custom icon #}
|
||||
{% include 'forms/partials/form_actions.html' with submit_icon='fas fa-check' %}
|
||||
```
|
||||
|
||||
## HTMX Integration
|
||||
|
||||
### Inline Validation
|
||||
|
||||
```django
|
||||
<form hx-post="/submit/" hx-target="#form-results">
|
||||
{% for field in form %}
|
||||
{% include 'forms/partials/form_field.html' with
|
||||
field=field
|
||||
hx_validate=True
|
||||
hx_validate_url='/validate/'
|
||||
%}
|
||||
{% endfor %}
|
||||
{% include 'forms/partials/form_actions.html' with show_loading=True %}
|
||||
</form>
|
||||
```
|
||||
|
||||
### Validation Endpoint Response
|
||||
|
||||
```django
|
||||
{# Success #}
|
||||
{% include 'forms/partials/field_success.html' with message='Valid!' %}
|
||||
|
||||
{# Error #}
|
||||
{% include 'forms/partials/field_error.html' with errors=errors %}
|
||||
```
|
||||
|
||||
## Custom Form Rendering
|
||||
|
||||
For complete control, use the components directly:
|
||||
|
||||
```django
|
||||
<form method="post" action="{% url 'submit' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Non-field errors #}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="mb-4 p-4 bg-red-50 rounded-lg" role="alert">
|
||||
{% for error in form.non_field_errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Custom field layout #}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% include 'forms/partials/form_field.html' with field=form.first_name %}
|
||||
{% include 'forms/partials/form_field.html' with field=form.last_name %}
|
||||
</div>
|
||||
|
||||
{% include 'forms/partials/form_field.html' with field=form.email %}
|
||||
|
||||
{% include 'forms/partials/form_actions.html' with submit_text='Create Account' %}
|
||||
</form>
|
||||
```
|
||||
|
||||
## CSS Classes
|
||||
|
||||
The form components use these CSS classes (defined in `components.css`):
|
||||
|
||||
- `.btn-primary` - Primary action button
|
||||
- `.btn-secondary` - Secondary action button
|
||||
- `.form-field` - Field wrapper
|
||||
- `.field-error` - Error message styling
|
||||
|
||||
## Accessibility
|
||||
|
||||
All form components include:
|
||||
|
||||
- Labels properly associated with inputs (`for`/`id`)
|
||||
- Required field indicators with screen reader text
|
||||
- Error messages with `role="alert"`
|
||||
- Help text linked via `aria-describedby`
|
||||
- Invalid state with `aria-invalid`
|
||||
106
backend/templates/forms/layouts/grid.html
Normal file
106
backend/templates/forms/layouts/grid.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% comment %}
|
||||
Grid Form Layout
|
||||
================
|
||||
|
||||
Renders form fields in a responsive grid layout.
|
||||
|
||||
Purpose:
|
||||
Provides a multi-column grid layout for forms with many fields.
|
||||
Responsive - adjusts columns based on screen size.
|
||||
|
||||
Usage Examples:
|
||||
2-column grid:
|
||||
{% include 'forms/layouts/grid.html' with form=form cols=2 %}
|
||||
|
||||
3-column grid:
|
||||
{% include 'forms/layouts/grid.html' with form=form cols=3 %}
|
||||
|
||||
With full-width fields:
|
||||
{% include 'forms/layouts/grid.html' with form=form cols=2 full_width='description,notes' %}
|
||||
|
||||
Parameters:
|
||||
Required:
|
||||
- form: Django form object
|
||||
|
||||
Optional:
|
||||
- cols: Number of columns (2 or 3, default: 2)
|
||||
- exclude: Comma-separated field names to exclude
|
||||
- fields: Comma-separated field names to include
|
||||
- full_width: Comma-separated field names that span full width
|
||||
- show_help: Show help text (default: True)
|
||||
- show_required: Show required indicator (default: True)
|
||||
- gap: Grid gap class (default: 'gap-4')
|
||||
- submit_text: Submit button text (default: 'Submit')
|
||||
- submit_class: Submit button CSS class
|
||||
- form_class: Additional CSS classes for form wrapper
|
||||
|
||||
Dependencies:
|
||||
- forms/partials/form_field.html
|
||||
- Tailwind CSS
|
||||
|
||||
Accessibility:
|
||||
- Responsive grid maintains logical order
|
||||
- Labels properly associated with inputs
|
||||
{% endcomment %}
|
||||
|
||||
{% load common_filters %}
|
||||
|
||||
{% with show_help=show_help|default:True show_required=show_required|default:True cols=cols|default:2 gap=gap|default:'gap-4' submit_text=submit_text|default:'Submit' %}
|
||||
|
||||
<div class="form-layout-grid {{ form_class }}">
|
||||
{# Non-field errors #}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="mb-4 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
|
||||
role="alert">
|
||||
<ul class="text-sm text-red-700 dark:text-red-300 space-y-1">
|
||||
{% for error in form.non_field_errors %}
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-exclamation-circle mt-0.5" aria-hidden="true"></i>
|
||||
{{ error }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Grid container #}
|
||||
<div class="grid {% if cols == 3 %}grid-cols-1 md:grid-cols-2 lg:grid-cols-3{% else %}grid-cols-1 md:grid-cols-2{% endif %} {{ gap }}">
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% elif fields %}
|
||||
{% if field.name in fields %}
|
||||
<div class="{% if full_width and field.name in full_width %}{% if cols == 3 %}md:col-span-2 lg:col-span-3{% else %}md:col-span-2{% endif %}{% endif %}">
|
||||
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif exclude %}
|
||||
{% if field.name not in exclude %}
|
||||
<div class="{% if full_width and field.name in full_width %}{% if cols == 3 %}md:col-span-2 lg:col-span-3{% else %}md:col-span-2{% endif %}{% endif %}">
|
||||
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="{% if full_width and field.name in full_width %}{% if cols == 3 %}md:col-span-2 lg:col-span-3{% else %}md:col-span-2{% endif %}{% endif %}">
|
||||
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Form actions - full width #}
|
||||
{% if show_actions|default:True %}
|
||||
<div class="flex items-center justify-end gap-3 mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
{% if show_cancel and cancel_url %}
|
||||
<a href="{{ cancel_url }}" class="btn-secondary">
|
||||
{{ cancel_text|default:'Cancel' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="{{ submit_class|default:'btn-primary' }}">
|
||||
{{ submit_text }}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endwith %}
|
||||
149
backend/templates/forms/layouts/inline.html
Normal file
149
backend/templates/forms/layouts/inline.html
Normal file
@@ -0,0 +1,149 @@
|
||||
{% comment %}
|
||||
Inline Form Layout
|
||||
==================
|
||||
|
||||
Renders form fields horizontally with labels inline with inputs.
|
||||
|
||||
Purpose:
|
||||
Provides an inline/horizontal form layout where labels appear
|
||||
to the left of inputs on larger screens.
|
||||
|
||||
Usage Examples:
|
||||
Basic inline form:
|
||||
{% include 'forms/layouts/inline.html' with form=form %}
|
||||
|
||||
Custom label width:
|
||||
{% include 'forms/layouts/inline.html' with form=form label_width='w-1/4' %}
|
||||
|
||||
Parameters:
|
||||
Required:
|
||||
- form: Django form object
|
||||
|
||||
Optional:
|
||||
- exclude: Comma-separated field names to exclude
|
||||
- fields: Comma-separated field names to include
|
||||
- label_width: Label column width class (default: 'w-1/3')
|
||||
- show_help: Show help text (default: True)
|
||||
- show_required: Show required indicator (default: True)
|
||||
- submit_text: Submit button text (default: 'Submit')
|
||||
- submit_class: Submit button CSS class (default: 'btn-primary')
|
||||
- show_cancel: Show cancel button (default: False)
|
||||
- cancel_url: URL for cancel button
|
||||
- form_class: Additional CSS classes for form wrapper
|
||||
|
||||
Dependencies:
|
||||
- forms/partials/form_field.html
|
||||
- Tailwind CSS
|
||||
|
||||
Accessibility:
|
||||
- Labels properly associated with inputs
|
||||
- Responsive - stacks on mobile
|
||||
{% endcomment %}
|
||||
|
||||
{% load common_filters %}
|
||||
|
||||
{% with show_help=show_help|default:True show_required=show_required|default:True label_width=label_width|default:'w-1/3' submit_text=submit_text|default:'Submit' %}
|
||||
|
||||
<div class="form-layout-inline {{ form_class }}">
|
||||
{# Non-field errors #}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="mb-4 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
|
||||
role="alert">
|
||||
<ul class="text-sm text-red-700 dark:text-red-300 space-y-1">
|
||||
{% for error in form.non_field_errors %}
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-exclamation-circle mt-0.5" aria-hidden="true"></i>
|
||||
{{ error }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Form fields - inline layout #}
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% elif fields %}
|
||||
{% if field.name in fields %}
|
||||
<div class="mb-4 sm:flex sm:items-start">
|
||||
<label for="{{ field.id_for_label }}"
|
||||
class="block mb-1 sm:mb-0 sm:{{ label_width }} sm:pt-2 sm:pr-4 sm:text-right text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ field.label }}
|
||||
{% if show_required and field.field.required %}
|
||||
<span class="text-red-500" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
{{ field|add_class:'w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white' }}
|
||||
{% if field.errors %}
|
||||
{% include 'forms/partials/field_error.html' with errors=field.errors %}
|
||||
{% endif %}
|
||||
{% if show_help and field.help_text %}
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif exclude %}
|
||||
{% if field.name not in exclude %}
|
||||
<div class="mb-4 sm:flex sm:items-start">
|
||||
<label for="{{ field.id_for_label }}"
|
||||
class="block mb-1 sm:mb-0 sm:{{ label_width }} sm:pt-2 sm:pr-4 sm:text-right text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ field.label }}
|
||||
{% if show_required and field.field.required %}
|
||||
<span class="text-red-500" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
{{ field|add_class:'w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white' }}
|
||||
{% if field.errors %}
|
||||
{% include 'forms/partials/field_error.html' with errors=field.errors %}
|
||||
{% endif %}
|
||||
{% if show_help and field.help_text %}
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="mb-4 sm:flex sm:items-start">
|
||||
<label for="{{ field.id_for_label }}"
|
||||
class="block mb-1 sm:mb-0 sm:{{ label_width }} sm:pt-2 sm:pr-4 sm:text-right text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ field.label }}
|
||||
{% if show_required and field.field.required %}
|
||||
<span class="text-red-500" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
{{ field|add_class:'w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white' }}
|
||||
{% if field.errors %}
|
||||
{% include 'forms/partials/field_error.html' with errors=field.errors %}
|
||||
{% endif %}
|
||||
{% if show_help and field.help_text %}
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# Form actions - aligned with inputs #}
|
||||
{% if show_actions|default:True %}
|
||||
<div class="sm:flex sm:items-center mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="sm:{{ label_width }}"></div>
|
||||
<div class="flex-1 flex items-center justify-start gap-3">
|
||||
<button type="submit" class="{{ submit_class|default:'btn-primary' }}">
|
||||
{{ submit_text }}
|
||||
</button>
|
||||
{% if show_cancel and cancel_url %}
|
||||
<a href="{{ cancel_url }}" class="btn-secondary">
|
||||
{{ cancel_text|default:'Cancel' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endwith %}
|
||||
106
backend/templates/forms/layouts/stacked.html
Normal file
106
backend/templates/forms/layouts/stacked.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% comment %}
|
||||
Stacked Form Layout
|
||||
===================
|
||||
|
||||
Renders form fields in a vertical stacked layout (default form layout).
|
||||
|
||||
Purpose:
|
||||
Provides a standard vertical form layout where each field takes
|
||||
full width with labels above inputs.
|
||||
|
||||
Usage Examples:
|
||||
Basic form:
|
||||
{% include 'forms/layouts/stacked.html' with form=form %}
|
||||
|
||||
With fieldsets:
|
||||
{% include 'forms/layouts/stacked.html' with form=form show_fieldsets=True %}
|
||||
|
||||
Exclude fields:
|
||||
{% include 'forms/layouts/stacked.html' with form=form exclude='password2' %}
|
||||
|
||||
Parameters:
|
||||
Required:
|
||||
- form: Django form object
|
||||
|
||||
Optional:
|
||||
- exclude: Comma-separated field names to exclude
|
||||
- fields: Comma-separated field names to include (if set, only these are shown)
|
||||
- show_fieldsets: Group fields by fieldset (default: False)
|
||||
- show_help: Show help text (default: True)
|
||||
- show_required: Show required indicator (default: True)
|
||||
- submit_text: Submit button text (default: 'Submit')
|
||||
- submit_class: Submit button CSS class (default: 'btn-primary')
|
||||
- show_cancel: Show cancel button (default: False)
|
||||
- cancel_url: URL for cancel button
|
||||
- cancel_text: Cancel button text (default: 'Cancel')
|
||||
- form_class: Additional CSS classes for form wrapper
|
||||
|
||||
Dependencies:
|
||||
- forms/partials/form_field.html
|
||||
- forms/partials/field_error.html
|
||||
- Tailwind CSS
|
||||
|
||||
Accessibility:
|
||||
- Uses fieldset/legend for grouped fields
|
||||
- Labels properly associated with inputs
|
||||
- Error summary at top for screen readers
|
||||
{% endcomment %}
|
||||
|
||||
{% load common_filters %}
|
||||
|
||||
{% with show_help=show_help|default:True show_required=show_required|default:True submit_text=submit_text|default:'Submit' %}
|
||||
|
||||
<div class="form-layout-stacked {{ form_class }}">
|
||||
{# Non-field errors at top #}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="mb-4 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
|
||||
role="alert">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="fas fa-exclamation-circle text-red-500 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Please correct the following errors:</h3>
|
||||
<ul class="mt-1 text-sm text-red-700 dark:text-red-300 list-disc list-inside">
|
||||
{% for error in form.non_field_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Form fields #}
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% elif fields %}
|
||||
{# Only show specified fields #}
|
||||
{% if field.name in fields %}
|
||||
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
|
||||
{% endif %}
|
||||
{% elif exclude %}
|
||||
{# Exclude specified fields #}
|
||||
{% if field.name not in exclude %}
|
||||
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% include 'forms/partials/form_field.html' with field=field show_help=show_help required_indicator=show_required %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# Form actions #}
|
||||
{% if show_actions|default:True %}
|
||||
<div class="flex items-center justify-end gap-3 mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
{% if show_cancel and cancel_url %}
|
||||
<a href="{{ cancel_url }}" class="btn-secondary {{ cancel_class }}">
|
||||
{{ cancel_text|default:'Cancel' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="{{ submit_class|default:'btn-primary' }}">
|
||||
{{ submit_text }}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endwith %}
|
||||
@@ -1,7 +1,57 @@
|
||||
{% comment %}
|
||||
Field Error Component
|
||||
=====================
|
||||
|
||||
Displays error messages for a form field with icon and proper accessibility.
|
||||
|
||||
Purpose:
|
||||
Renders error messages in a consistent, accessible format with visual
|
||||
indicators. Used within form_field.html or standalone.
|
||||
|
||||
Usage Examples:
|
||||
Within form_field.html (automatic):
|
||||
{% include 'forms/partials/field_error.html' with errors=field.errors %}
|
||||
|
||||
Standalone:
|
||||
{% include 'forms/partials/field_error.html' with errors=form.email.errors %}
|
||||
|
||||
Non-field errors:
|
||||
{% include 'forms/partials/field_error.html' with errors=form.non_field_errors %}
|
||||
|
||||
Parameters:
|
||||
Required:
|
||||
- errors: List of error messages (typically field.errors)
|
||||
|
||||
Optional:
|
||||
- show_icon: Show error icon (default: True)
|
||||
- animate: Add entrance animation (default: True)
|
||||
- size: 'sm', 'md', 'lg' (default: 'sm')
|
||||
|
||||
Dependencies:
|
||||
- Tailwind CSS for styling
|
||||
- Font Awesome icons
|
||||
|
||||
Accessibility:
|
||||
- Uses role="alert" for immediate screen reader announcement
|
||||
- aria-live="assertive" for dynamic error display
|
||||
- Error icon is decorative (aria-hidden)
|
||||
{% endcomment %}
|
||||
|
||||
{% with show_icon=show_icon|default:True animate=animate|default:True size=size|default:'sm' %}
|
||||
|
||||
{% if errors %}
|
||||
<ul class="field-errors">
|
||||
{% for e in errors %}
|
||||
<li>{{ e }}</li>
|
||||
<ul class="{% if size == 'lg' %}text-base{% elif size == 'md' %}text-sm{% else %}text-xs{% endif %} text-red-600 dark:text-red-400 space-y-1 {% if animate %}animate-slide-down{% endif %}"
|
||||
role="alert"
|
||||
aria-live="assertive">
|
||||
{% for error in errors %}
|
||||
<li class="flex items-start gap-1.5">
|
||||
{% if show_icon %}
|
||||
<i class="fas fa-exclamation-circle mt-0.5 flex-shrink-0" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
<span>{{ error }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
|
||||
@@ -1 +1,48 @@
|
||||
<div class="field-success" aria-hidden="true">✓</div>
|
||||
{% comment %}
|
||||
Field Success Component
|
||||
=======================
|
||||
|
||||
Displays success message for a validated form field with icon.
|
||||
|
||||
Purpose:
|
||||
Renders a success indicator when a field passes validation.
|
||||
Typically used with HTMX inline validation.
|
||||
|
||||
Usage Examples:
|
||||
After successful validation:
|
||||
{% include 'forms/partials/field_success.html' with message='Username is available' %}
|
||||
|
||||
Simple checkmark (no message):
|
||||
{% include 'forms/partials/field_success.html' %}
|
||||
|
||||
Parameters:
|
||||
Optional:
|
||||
- message: Success message text (if empty, shows only icon)
|
||||
- show_icon: Show success icon (default: True)
|
||||
- animate: Add entrance animation (default: True)
|
||||
- size: 'sm', 'md', 'lg' (default: 'sm')
|
||||
|
||||
Dependencies:
|
||||
- Tailwind CSS for styling
|
||||
- Font Awesome icons
|
||||
|
||||
Accessibility:
|
||||
- Uses role="status" for polite screen reader announcement
|
||||
- aria-live="polite" for non-urgent updates
|
||||
- Success icon is decorative (aria-hidden)
|
||||
{% endcomment %}
|
||||
|
||||
{% with show_icon=show_icon|default:True animate=animate|default:True size=size|default:'sm' %}
|
||||
|
||||
<div class="{% if size == 'lg' %}text-base{% elif size == 'md' %}text-sm{% else %}text-xs{% endif %} text-green-600 dark:text-green-400 flex items-center gap-1.5 {% if animate %}animate-slide-down{% endif %}"
|
||||
role="status"
|
||||
aria-live="polite">
|
||||
{% if show_icon %}
|
||||
<i class="fas fa-check-circle flex-shrink-0" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
<span>{{ message }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endwith %}
|
||||
|
||||
@@ -1,4 +1,135 @@
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn-primary">Save</button>
|
||||
<button type="button" class="btn-secondary" hx-trigger="click" hx-swap="none">Cancel</button>
|
||||
{% comment %}
|
||||
Form Actions Component
|
||||
======================
|
||||
|
||||
Renders form submit and cancel buttons with consistent styling.
|
||||
|
||||
Purpose:
|
||||
Provides a standardized form actions section with submit button,
|
||||
optional cancel button, and loading state support.
|
||||
|
||||
Usage Examples:
|
||||
Basic submit:
|
||||
{% include 'forms/partials/form_actions.html' %}
|
||||
|
||||
With cancel:
|
||||
{% include 'forms/partials/form_actions.html' with show_cancel=True cancel_url='/list/' %}
|
||||
|
||||
Custom text:
|
||||
{% include 'forms/partials/form_actions.html' with submit_text='Save Changes' %}
|
||||
|
||||
With loading state:
|
||||
{% include 'forms/partials/form_actions.html' with show_loading=True loading_id='submit-loading' %}
|
||||
|
||||
Right-aligned (default):
|
||||
{% include 'forms/partials/form_actions.html' %}
|
||||
|
||||
Left-aligned:
|
||||
{% include 'forms/partials/form_actions.html' with align='left' %}
|
||||
|
||||
HTMX form:
|
||||
{% include 'forms/partials/form_actions.html' with hx_disable='true' %}
|
||||
|
||||
Parameters:
|
||||
Optional (submit):
|
||||
- submit_text: Submit button text (default: 'Save')
|
||||
- submit_class: CSS class (default: 'btn-primary')
|
||||
- submit_disabled: Disable submit button (default: False)
|
||||
- submit_icon: Icon class for submit button (e.g., 'fas fa-check')
|
||||
|
||||
Optional (cancel):
|
||||
- show_cancel: Show cancel button (default: False)
|
||||
- cancel_url: URL for cancel link
|
||||
- cancel_text: Cancel button text (default: 'Cancel')
|
||||
- cancel_class: CSS class (default: 'btn-secondary')
|
||||
|
||||
Optional (loading):
|
||||
- show_loading: Show loading indicator on submit (default: False)
|
||||
- loading_id: ID for htmx-indicator (default: 'submit-loading')
|
||||
|
||||
Optional (layout):
|
||||
- align: 'left', 'right', 'center', 'between' (default: 'right')
|
||||
- show_border: Show top border (default: True)
|
||||
|
||||
Optional (HTMX):
|
||||
- hx_disable: Add hx-disable on submit (default: False)
|
||||
|
||||
Dependencies:
|
||||
- Tailwind CSS
|
||||
- HTMX (optional, for loading states)
|
||||
- Font Awesome (optional, for icons)
|
||||
|
||||
Accessibility:
|
||||
- Submit button is properly typed
|
||||
- Loading state announced to screen readers
|
||||
{% endcomment %}
|
||||
|
||||
{% with submit_text=submit_text|default:'Save' cancel_text=cancel_text|default:'Cancel' align=align|default:'right' show_border=show_border|default:True %}
|
||||
|
||||
<div class="form-actions flex items-center gap-3 mt-6
|
||||
{% if show_border %}pt-4 border-t border-gray-200 dark:border-gray-700{% endif %}
|
||||
{% if align == 'left' %}justify-start
|
||||
{% elif align == 'center' %}justify-center
|
||||
{% elif align == 'between' %}justify-between
|
||||
{% else %}justify-end{% endif %}">
|
||||
|
||||
{# Cancel button (left side for 'between' alignment) #}
|
||||
{% if show_cancel and align == 'between' %}
|
||||
{% if cancel_url %}
|
||||
<a href="{{ cancel_url }}"
|
||||
class="{{ cancel_class|default:'btn-secondary' }}">
|
||||
{{ cancel_text }}
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button"
|
||||
class="{{ cancel_class|default:'btn-secondary' }}"
|
||||
onclick="history.back()">
|
||||
{{ cancel_text }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Button group #}
|
||||
<div class="flex items-center gap-3">
|
||||
{# Cancel button (for non-between alignment) #}
|
||||
{% if show_cancel and align != 'between' %}
|
||||
{% if cancel_url %}
|
||||
<a href="{{ cancel_url }}"
|
||||
class="{{ cancel_class|default:'btn-secondary' }}">
|
||||
{{ cancel_text }}
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button"
|
||||
class="{{ cancel_class|default:'btn-secondary' }}"
|
||||
onclick="history.back()">
|
||||
{{ cancel_text }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Submit button #}
|
||||
<button type="submit"
|
||||
class="{{ submit_class|default:'btn-primary' }} relative"
|
||||
{% if submit_disabled %}disabled{% endif %}
|
||||
{% if hx_disable %}hx-disable{% endif %}>
|
||||
{# Icon (optional) #}
|
||||
{% if submit_icon %}
|
||||
<i class="{{ submit_icon }} mr-2" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
|
||||
{# Text #}
|
||||
<span>{{ submit_text }}</span>
|
||||
|
||||
{# Loading indicator (optional) #}
|
||||
{% if show_loading %}
|
||||
<span id="{{ loading_id|default:'submit-loading' }}"
|
||||
class="htmx-indicator ml-2">
|
||||
<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>
|
||||
<span class="sr-only">Submitting...</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endwith %}
|
||||
|
||||
@@ -1,5 +1,198 @@
|
||||
<div class="form-field" data-field-name="{{ field.name }}">
|
||||
<label for="id_{{ field.name }}">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
<div class="field-feedback" aria-live="polite">{% include "forms/partials/field_error.html" %}</div>
|
||||
{% comment %}
|
||||
Form Field Component
|
||||
====================
|
||||
|
||||
A comprehensive form field component with label, input, help text, and error handling.
|
||||
|
||||
Purpose:
|
||||
Renders a complete form field with consistent styling, accessibility attributes,
|
||||
and optional HTMX validation support.
|
||||
|
||||
Usage Examples:
|
||||
Basic 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 without label:
|
||||
{% include 'forms/partials/form_field.html' with field=form.hidden_field show_label=False %}
|
||||
|
||||
Field with HTMX validation:
|
||||
{% include 'forms/partials/form_field.html' with field=form.username hx_validate=True hx_validate_url='/api/validate-username/' %}
|
||||
|
||||
Inline field:
|
||||
{% include 'forms/partials/form_field.html' with field=form.email layout='inline' %}
|
||||
|
||||
With success state:
|
||||
{% include 'forms/partials/form_field.html' with field=form.email show_success=True %}
|
||||
|
||||
Parameters:
|
||||
Required:
|
||||
- field: Django form field object
|
||||
|
||||
Optional (labels):
|
||||
- label: Custom label text (default: field.label)
|
||||
- show_label: Show label (default: True)
|
||||
- label_class: Additional CSS classes for label
|
||||
|
||||
Optional (input):
|
||||
- input_class: Additional CSS classes for input
|
||||
- placeholder: Custom placeholder text
|
||||
- autofocus: Add autofocus attribute (default: False)
|
||||
- disabled: Disable input (default: False)
|
||||
- readonly: Make input readonly (default: False)
|
||||
|
||||
Optional (help/errors):
|
||||
- help_text: Custom help text (default: field.help_text)
|
||||
- show_help: Show help text (default: True)
|
||||
- show_errors: Show error messages (default: True)
|
||||
- show_success: Show success indicator when valid (default: False)
|
||||
|
||||
Optional (HTMX validation):
|
||||
- hx_validate: Enable HTMX inline validation (default: False)
|
||||
- hx_validate_url: URL for validation endpoint
|
||||
- hx_validate_trigger: Trigger event (default: 'blur changed delay:500ms')
|
||||
- hx_validate_target: Target for validation response (default: auto-generated)
|
||||
|
||||
Optional (layout):
|
||||
- layout: 'stacked' or 'inline' (default: 'stacked')
|
||||
- required_indicator: Show required indicator (default: True)
|
||||
- size: 'sm', 'md', 'lg' for input size (default: 'md')
|
||||
|
||||
Dependencies:
|
||||
- Tailwind CSS for styling
|
||||
- HTMX (optional, for inline validation)
|
||||
- Alpine.js (optional, for enhanced interactions)
|
||||
- common_filters template tags (for add_class filter)
|
||||
|
||||
Accessibility:
|
||||
- Label properly associated with input via for/id
|
||||
- aria-describedby links help text and errors to input
|
||||
- aria-invalid set when field has errors
|
||||
- aria-required set for required fields
|
||||
- Error messages use role="alert" for screen reader announcement
|
||||
{% endcomment %}
|
||||
{% load common_filters %}
|
||||
|
||||
{% with show_label=show_label|default:True show_help=show_help|default:True show_errors=show_errors|default:True show_success=show_success|default:False required_indicator=required_indicator|default:True layout=layout|default:'stacked' size=size|default:'md' %}
|
||||
|
||||
<div class="form-field {% if layout == 'inline' %}flex items-center gap-4{% else %}mb-4{% endif %}"
|
||||
data-field-name="{{ field.name }}"
|
||||
data-field-required="{{ field.field.required|yesno:'true,false' }}"
|
||||
{% if hx_validate %}x-data="{ valid: null, validating: false, touched: false }"{% endif %}>
|
||||
|
||||
{# Label #}
|
||||
{% if show_label %}
|
||||
<label for="{{ field.id_for_label }}"
|
||||
class="{% if layout == 'inline' %}w-1/3 text-right{% else %}block mb-1.5{% endif %} {% if size == 'sm' %}text-xs{% elif size == 'lg' %}text-base{% else %}text-sm{% endif %} font-medium text-foreground {{ label_class }}">
|
||||
{{ label|default:field.label }}
|
||||
{% if required_indicator and field.field.required %}
|
||||
<span class="text-destructive ml-0.5" aria-hidden="true">*</span>
|
||||
<span class="sr-only">(required)</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endif %}
|
||||
|
||||
{# Input wrapper #}
|
||||
<div class="{% if layout == 'inline' and show_label %}flex-1{% else %}w-full{% endif %} relative">
|
||||
{# Field input with enhanced attributes #}
|
||||
{# Base widget classes for sizing, spacing, and transitions #}
|
||||
{% with base_class='w-full border rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-0 bg-background text-foreground placeholder:text-muted-foreground' %}
|
||||
{# Size classes #}
|
||||
{% with size_class=size|default:'md'|yesno:'px-2.5 py-1.5 text-xs,px-3 py-2 text-sm,px-4 py-2.5 text-base' %}
|
||||
{% if size == 'sm' %}{% with actual_size='px-2.5 py-1.5 text-xs' %}
|
||||
{# Error-specific classes override border and focus ring colors #}
|
||||
{% with error_class='border-destructive focus:ring-destructive/30 focus:border-destructive' %}
|
||||
{# Success-specific classes #}
|
||||
{% with success_class='border-green-500 focus:ring-green-500/30 focus:border-green-500' %}
|
||||
{# Normal state classes for border and focus ring #}
|
||||
{% with normal_class='border-input focus:ring-ring/30 focus:border-ring' %}
|
||||
|
||||
{# Render the field with base classes plus state-specific classes #}
|
||||
{% if field.errors %}
|
||||
{{ field|add_class:base_class|add_class:actual_size|add_class:error_class|add_class:input_class }}
|
||||
{% else %}
|
||||
{{ field|add_class:base_class|add_class:actual_size|add_class:normal_class|add_class:input_class }}
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
|
||||
{% elif size == 'lg' %}{% with actual_size='px-4 py-2.5 text-base' %}
|
||||
{% with error_class='border-destructive focus:ring-destructive/30 focus:border-destructive' %}
|
||||
{% with success_class='border-green-500 focus:ring-green-500/30 focus:border-green-500' %}
|
||||
{% with normal_class='border-input focus:ring-ring/30 focus:border-ring' %}
|
||||
{% if field.errors %}
|
||||
{{ field|add_class:base_class|add_class:actual_size|add_class:error_class|add_class:input_class }}
|
||||
{% else %}
|
||||
{{ field|add_class:base_class|add_class:actual_size|add_class:normal_class|add_class:input_class }}
|
||||
{% endif %}
|
||||
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
|
||||
{% else %}{% with actual_size='px-3 py-2 text-sm' %}
|
||||
{% with error_class='border-destructive focus:ring-destructive/30 focus:border-destructive' %}
|
||||
{% with success_class='border-green-500 focus:ring-green-500/30 focus:border-green-500' %}
|
||||
{% with normal_class='border-input focus:ring-ring/30 focus:border-ring' %}
|
||||
{% if field.errors %}
|
||||
{{ field|add_class:base_class|add_class:actual_size|add_class:error_class|add_class:input_class }}
|
||||
{% else %}
|
||||
{{ field|add_class:base_class|add_class:actual_size|add_class:normal_class|add_class:input_class }}
|
||||
{% endif %}
|
||||
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{# HTMX validation attributes (when enabled) #}
|
||||
{% if hx_validate and hx_validate_url %}
|
||||
<script>
|
||||
(function() {
|
||||
var el = document.getElementById('{{ field.id_for_label }}');
|
||||
if (el) {
|
||||
el.setAttribute('hx-post', '{{ hx_validate_url }}');
|
||||
el.setAttribute('hx-trigger', '{{ hx_validate_trigger|default:"blur changed delay:500ms" }}');
|
||||
el.setAttribute('hx-target', '#{{ field.name }}-feedback');
|
||||
el.setAttribute('hx-swap', 'innerHTML');
|
||||
el.setAttribute('hx-indicator', '#{{ field.name }}-indicator');
|
||||
el.setAttribute('hx-include', '[name="{{ field.name }}"]');
|
||||
if (typeof htmx !== 'undefined') htmx.process(el);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{# Validation indicator (for HTMX) - positioned inside input #}
|
||||
{% if hx_validate %}
|
||||
<div id="{{ field.name }}-indicator" class="htmx-indicator absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<i class="fas fa-spinner fa-spin text-muted-foreground" aria-hidden="true"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Success indicator (shown when valid and no errors) #}
|
||||
{% if show_success and not field.errors and field.value %}
|
||||
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<i class="fas fa-check-circle text-green-500" aria-hidden="true"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Feedback area (errors, success, help) #}
|
||||
<div id="{{ field.name }}-feedback"
|
||||
class="field-feedback mt-1.5"
|
||||
aria-live="polite"
|
||||
aria-atomic="true">
|
||||
|
||||
{# Error messages #}
|
||||
{% if show_errors and field.errors %}
|
||||
{% include 'forms/partials/field_error.html' with errors=field.errors %}
|
||||
{% endif %}
|
||||
|
||||
{# Help text #}
|
||||
{% if show_help and field.help_text %}
|
||||
<p id="{{ field.name }}-help"
|
||||
class="{% if size == 'sm' %}text-xs{% elif size == 'lg' %}text-sm{% else %}text-xs{% endif %} text-muted-foreground {% if field.errors %}mt-1{% endif %}">
|
||||
{{ help_text|default:field.help_text }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endwith %}
|
||||
|
||||
194
backend/templates/forms/partials/form_submission_feedback.html
Normal file
194
backend/templates/forms/partials/form_submission_feedback.html
Normal file
@@ -0,0 +1,194 @@
|
||||
{% comment %}
|
||||
Form Submission Feedback Component
|
||||
==================================
|
||||
|
||||
Displays feedback after form submission with various states.
|
||||
|
||||
Purpose:
|
||||
Provides consistent feedback UI for form submission results including
|
||||
success confirmations, error summaries, and loading states.
|
||||
|
||||
Usage Examples:
|
||||
Success state:
|
||||
{% include 'forms/partials/form_submission_feedback.html' with state='success' message='Park saved successfully!' %}
|
||||
|
||||
Error state:
|
||||
{% include 'forms/partials/form_submission_feedback.html' with state='error' message='Please fix the errors below' %}
|
||||
|
||||
Loading state:
|
||||
{% include 'forms/partials/form_submission_feedback.html' with state='loading' message='Saving...' %}
|
||||
|
||||
With redirect countdown:
|
||||
{% include 'forms/partials/form_submission_feedback.html' with state='success' message='Saved!' redirect_url='/parks/' redirect_seconds=3 %}
|
||||
|
||||
Parameters:
|
||||
Required:
|
||||
- state: 'success', 'error', 'warning', 'loading' (default: 'success')
|
||||
- message: Main feedback message
|
||||
|
||||
Optional:
|
||||
- title: Optional title text
|
||||
- show_icon: Show status icon (default: True)
|
||||
- show_actions: Show action buttons (default: True)
|
||||
- primary_action_text: Primary button text (default: varies by state)
|
||||
- primary_action_url: Primary button URL
|
||||
- secondary_action_text: Secondary button text
|
||||
- secondary_action_url: Secondary button URL
|
||||
- redirect_url: URL to redirect to after countdown
|
||||
- redirect_seconds: Seconds before redirect (default: 3)
|
||||
- errors: List of error messages (for error state)
|
||||
- animate: Enable animations (default: True)
|
||||
|
||||
Dependencies:
|
||||
- Tailwind CSS for styling
|
||||
- Alpine.js for countdown functionality
|
||||
- Font Awesome icons
|
||||
|
||||
Accessibility:
|
||||
- Uses appropriate ARIA roles based on state
|
||||
- Announces changes to screen readers
|
||||
{% endcomment %}
|
||||
|
||||
{% with state=state|default:'success' show_icon=show_icon|default:True show_actions=show_actions|default:True animate=animate|default:True redirect_seconds=redirect_seconds|default:3 %}
|
||||
|
||||
<div class="form-submission-feedback rounded-lg p-4 {% if animate %}animate-fade-in{% endif %}
|
||||
{% if state == 'success' %}bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800
|
||||
{% elif state == 'error' %}bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800
|
||||
{% elif state == 'warning' %}bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800
|
||||
{% elif state == 'loading' %}bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800
|
||||
{% endif %}"
|
||||
role="{% if state == 'error' %}alert{% else %}status{% endif %}"
|
||||
aria-live="{% if state == 'error' %}assertive{% else %}polite{% endif %}"
|
||||
{% if redirect_url %}
|
||||
x-data="{ countdown: {{ redirect_seconds }} }"
|
||||
x-init="setInterval(() => { if(countdown > 0) countdown--; else window.location.href = '{{ redirect_url }}'; }, 1000)"
|
||||
{% endif %}>
|
||||
|
||||
<div class="flex {% if title or errors %}flex-col gap-3{% else %}items-center gap-3{% endif %}">
|
||||
{# Icon #}
|
||||
{% if show_icon %}
|
||||
<div class="flex-shrink-0 {% if title or errors %}flex items-center gap-3{% endif %}">
|
||||
{% if state == 'success' %}
|
||||
<div class="w-10 h-10 rounded-full bg-green-100 dark:bg-green-800/30 flex items-center justify-center">
|
||||
<i class="fas fa-check text-green-600 dark:text-green-400 text-lg" aria-hidden="true"></i>
|
||||
</div>
|
||||
{% elif state == 'error' %}
|
||||
<div class="w-10 h-10 rounded-full bg-red-100 dark:bg-red-800/30 flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-circle text-red-600 dark:text-red-400 text-lg" aria-hidden="true"></i>
|
||||
</div>
|
||||
{% elif state == 'warning' %}
|
||||
<div class="w-10 h-10 rounded-full bg-yellow-100 dark:bg-yellow-800/30 flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 dark:text-yellow-400 text-lg" aria-hidden="true"></i>
|
||||
</div>
|
||||
{% elif state == 'loading' %}
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-800/30 flex items-center justify-center">
|
||||
<i class="fas fa-spinner fa-spin text-blue-600 dark:text-blue-400 text-lg" aria-hidden="true"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Title (if provided) #}
|
||||
{% if title %}
|
||||
<h3 class="font-semibold
|
||||
{% if state == 'success' %}text-green-800 dark:text-green-200
|
||||
{% elif state == 'error' %}text-red-800 dark:text-red-200
|
||||
{% elif state == 'warning' %}text-yellow-800 dark:text-yellow-200
|
||||
{% elif state == 'loading' %}text-blue-800 dark:text-blue-200
|
||||
{% endif %}">
|
||||
{{ title }}
|
||||
</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Content #}
|
||||
<div class="flex-1 min-w-0">
|
||||
{# Main message #}
|
||||
<p class="text-sm
|
||||
{% if state == 'success' %}text-green-700 dark:text-green-300
|
||||
{% elif state == 'error' %}text-red-700 dark:text-red-300
|
||||
{% elif state == 'warning' %}text-yellow-700 dark:text-yellow-300
|
||||
{% elif state == 'loading' %}text-blue-700 dark:text-blue-300
|
||||
{% endif %}">
|
||||
{{ message }}
|
||||
</p>
|
||||
|
||||
{# Error list (for error state) #}
|
||||
{% if state == 'error' and errors %}
|
||||
<ul class="mt-2 text-sm text-red-600 dark:text-red-400 space-y-1 list-disc list-inside">
|
||||
{% for error in errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{# Redirect countdown #}
|
||||
{% if redirect_url %}
|
||||
<p class="mt-2 text-xs
|
||||
{% if state == 'success' %}text-green-600 dark:text-green-400
|
||||
{% else %}text-muted-foreground
|
||||
{% endif %}">
|
||||
Redirecting in <span x-text="countdown"></span> seconds...
|
||||
<a href="{{ redirect_url }}" class="underline hover:no-underline ml-1">Go now</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Actions #}
|
||||
{% if show_actions and state != 'loading' %}
|
||||
<div class="flex-shrink-0 flex items-center gap-2 {% if title or errors %}mt-2{% endif %}">
|
||||
{% if state == 'error' %}
|
||||
{# Error state actions #}
|
||||
{% if secondary_action_url %}
|
||||
<a href="{{ secondary_action_url }}"
|
||||
class="btn btn-sm btn-ghost text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-800/30">
|
||||
{{ secondary_action_text|default:'Cancel' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if primary_action_url %}
|
||||
<a href="{{ primary_action_url }}"
|
||||
class="btn btn-sm bg-red-600 hover:bg-red-700 text-white">
|
||||
{{ primary_action_text|default:'Try Again' }}
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="submit"
|
||||
class="btn btn-sm bg-red-600 hover:bg-red-700 text-white">
|
||||
{{ primary_action_text|default:'Try Again' }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% elif state == 'success' %}
|
||||
{# Success state actions #}
|
||||
{% if secondary_action_url %}
|
||||
<a href="{{ secondary_action_url }}"
|
||||
class="btn btn-sm btn-ghost text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800/30">
|
||||
{{ secondary_action_text|default:'Add Another' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if primary_action_url %}
|
||||
<a href="{{ primary_action_url }}"
|
||||
class="btn btn-sm bg-green-600 hover:bg-green-700 text-white">
|
||||
{{ primary_action_text|default:'View' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% elif state == 'warning' %}
|
||||
{# Warning state actions #}
|
||||
{% if secondary_action_url %}
|
||||
<a href="{{ secondary_action_url }}"
|
||||
class="btn btn-sm btn-ghost">
|
||||
{{ secondary_action_text|default:'Cancel' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if primary_action_url %}
|
||||
<a href="{{ primary_action_url }}"
|
||||
class="btn btn-sm bg-yellow-600 hover:bg-yellow-700 text-white">
|
||||
{{ primary_action_text|default:'Continue' }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endwith %}
|
||||
Reference in New Issue
Block a user