# 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.

```javascript
// 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.

```javascript
// 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.

```javascript
// 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

```html
<button hx-post="/api/items/create/"
        hx-target="#items-list"
        hx-swap="innerHTML"
        hx-indicator="#loading">
    Create Item
</button>
```

### Form Submission

```html
<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

```html
<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

```html
<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

```html
<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

```javascript
// 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

```javascript
// 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

```html
<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

```html
<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:

```css
.htmx-request .loading-bar {
    display: block;
}
```

## Focus Management

### Modal Focus Trap

Modals automatically trap focus within their bounds:

```html
{% 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:

```javascript
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

```javascript
// 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

```html
<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

```html
<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

```html
<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:

```html
<!-- Works with or without JS -->
<form method="post" action="/items/create/"
      hx-post="/items/create/"
      hx-target="#form-container">
    <!-- Form content -->
</form>
```

### Error Recovery

```javascript
// 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

```javascript
window.addEventListener('offline', () => {
    Alpine.store('toast').warning('You are offline', 0);
});

window.addEventListener('online', () => {
    Alpine.store('toast').success('Connection restored');
});
```
