mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 08:11:09 -05:00
8.2 KiB
8.2 KiB
ThrillWiki Interaction Patterns
This document describes the standardized interaction patterns used throughout ThrillWiki.
Alpine.js Stores
ThrillWiki uses Alpine.js stores for global client-side state management.
Toast Store
The toast store manages notification messages displayed to users.
// Show a success toast
Alpine.store('toast').success('Park saved successfully!');
// Show an error toast
Alpine.store('toast').error('Failed to save park');
// Show a warning toast
Alpine.store('toast').warning('Your session will expire soon');
// Show an info toast
Alpine.store('toast').info('New features are available');
// Toast with custom duration (in ms)
Alpine.store('toast').success('Quick message', 2000);
// Toast with action button
Alpine.store('toast').success('Item deleted', 5000, {
action: {
label: 'Undo',
onClick: () => undoDelete()
}
});
// Persistent toast (duration = 0)
Alpine.store('toast').error('Connection lost', 0);
Theme Store
The theme store manages dark/light mode preferences.
// Get current theme
const isDark = Alpine.store('theme').isDark;
// Toggle theme
Alpine.store('theme').toggle();
// Set specific theme
Alpine.store('theme').set('dark');
Alpine.store('theme').set('light');
Alpine.store('theme').set('system');
Auth Store
The auth store tracks user authentication state.
// Check if user is authenticated
if (Alpine.store('auth').isAuthenticated) {
// Show authenticated content
}
// Get current user
const user = Alpine.store('auth').user;
HTMX Patterns
Standard Request Pattern
<button hx-post="/api/items/create/"
hx-target="#items-list"
hx-swap="innerHTML"
hx-indicator="#loading">
Create Item
</button>
Form Submission
<form hx-post="/items/create/"
hx-target="#form-container"
hx-swap="outerHTML"
hx-indicator=".form-loading">
{% csrf_token %}
<!-- Form fields -->
<button type="submit">Save</button>
</form>
Inline Validation
<input type="email"
name="email"
hx-post="/validate/email/"
hx-trigger="blur changed delay:500ms"
hx-target="next .field-feedback"
hx-swap="innerHTML">
<div class="field-feedback"></div>
Infinite Scroll
<div id="items-container">
{% for item in items %}
{% include 'items/_card.html' %}
{% endfor %}
{% if has_next %}
<div hx-get="?page={{ next_page }}"
hx-trigger="revealed"
hx-swap="outerHTML"
hx-indicator="#scroll-loading">
<div id="scroll-loading" class="htmx-indicator">
Loading more...
</div>
</div>
{% endif %}
</div>
Modal Loading
<button hx-get="/items/{{ item.id }}/edit/"
hx-target="#modal-container"
hx-swap="innerHTML"
@click="showModal = true">
Edit
</button>
<div id="modal-container" x-show="showModal">
<!-- Modal content loaded here -->
</div>
Event Handling
HTMX Events
// Before request
document.body.addEventListener('htmx:beforeRequest', (event) => {
// Show loading state
});
// After successful swap
document.body.addEventListener('htmx:afterSwap', (event) => {
// Initialize new content
});
// Handle errors
document.body.addEventListener('htmx:responseError', (event) => {
Alpine.store('toast').error('Request failed');
});
Custom Events
// Trigger from server via HX-Trigger header
document.body.addEventListener('showToast', (event) => {
const { type, message, duration } = event.detail;
Alpine.store('toast')[type](message, duration);
});
// Close modal event
document.body.addEventListener('closeModal', () => {
Alpine.store('modal').close();
});
// Refresh section event
document.body.addEventListener('refreshSection', (event) => {
const { target, url } = event.detail;
htmx.ajax('GET', url, target);
});
Loading States
Button Loading
<button type="submit"
x-data="{ loading: false }"
@click="loading = true"
:disabled="loading"
:class="{ 'opacity-50 cursor-not-allowed': loading }">
<span x-show="!loading">Save</span>
<span x-show="loading" class="flex items-center gap-2">
<i class="fas fa-spinner fa-spin"></i>
Saving...
</span>
</button>
Section Loading
<div id="content"
hx-get="/content/"
hx-trigger="load"
hx-indicator="#content-loading">
<div id="content-loading" class="htmx-indicator">
{% include 'components/skeletons/list_skeleton.html' %}
</div>
</div>
Global Loading Bar
The application includes a global loading bar that appears during HTMX requests:
.htmx-request .loading-bar {
display: block;
}
Focus Management
Modal Focus Trap
Modals automatically trap focus within their bounds:
{% include 'components/modals/modal_base.html' with
modal_id='my-modal'
show_var='showModal'
title='Modal Title'
%}
The modal component handles:
- Focus moves to first focusable element on open
- Tab cycles through modal elements only
- Escape key closes modal
- Focus returns to trigger element on close
Form Focus
After validation errors, focus moves to the first invalid field:
document.body.addEventListener('htmx:afterSwap', (event) => {
const firstError = event.target.querySelector('.field-error input');
if (firstError) {
firstError.focus();
}
});
Keyboard Navigation
Standard Shortcuts
| Key | Action |
|---|---|
Escape |
Close modal/dropdown |
Enter |
Submit form / Activate button |
Tab |
Move to next focusable element |
Shift+Tab |
Move to previous focusable element |
Arrow Up/Down |
Navigate dropdown options |
Custom Shortcuts
// Global keyboard shortcuts
document.addEventListener('keydown', (event) => {
// Ctrl/Cmd + K: Open search
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault();
Alpine.store('search').open();
}
});
Animation Patterns
Entry Animations
<div x-show="visible"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95">
Content
</div>
Slide Animations
<div x-show="visible"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4">
Content
</div>
Stagger Animations
<template x-for="(item, index) in items" :key="item.id">
<div x-show="visible"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
:style="{ transitionDelay: `${index * 50}ms` }">
<!-- Item content -->
</div>
</template>
Error Handling
Graceful Degradation
All interactions work without JavaScript:
<!-- Works with or without JS -->
<form method="post" action="/items/create/"
hx-post="/items/create/"
hx-target="#form-container">
<!-- Form content -->
</form>
Error Recovery
// Retry failed requests
document.body.addEventListener('htmx:responseError', (event) => {
const { xhr, target } = event.detail;
if (xhr.status >= 500) {
Alpine.store('toast').error('Server error. Please try again.', 0, {
action: {
label: 'Retry',
onClick: () => htmx.trigger(target, 'htmx:load')
}
});
}
});
Offline Handling
window.addEventListener('offline', () => {
Alpine.store('toast').warning('You are offline', 0);
});
window.addEventListener('online', () => {
Alpine.store('toast').success('Connection restored');
});