mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 15:51:09 -05:00
- Implemented park detail page with dynamic content loading for rides and weather. - Created park list page with filters and search functionality. - Developed ride detail page showcasing ride stats, reviews, and similar rides. - Added ride list page with filtering options and dynamic loading. - Introduced search results page with tabs for parks, rides, and users. - Added HTMX tests for global search functionality.
1050 lines
30 KiB
Markdown
1050 lines
30 KiB
Markdown
# ThrillWiki Frontend Rewrite Plan
|
|
## HTMX + Alpine.js Enhanced Implementation
|
|
|
|
**Created:** 2025-12-20
|
|
**Status:** Planning
|
|
**Scope:** Complete frontend redesign for enhanced design, modularity, and feature parity
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
This plan outlines a comprehensive rewrite of the ThrillWiki HTMX/Alpine.js frontend to achieve:
|
|
1. **Enhanced Design** - Modern UI with Shadcn UI patterns, animations, and polished UX
|
|
2. **Improved Modularity** - Reusable component architecture with clear separation of concerns
|
|
3. **Feature Parity** - Full functionality matching the previous React frontend
|
|
|
|
### Current State Analysis
|
|
|
|
Based on the [CRITICAL_ANALYSIS_HTMX_ALPINE.md](../docs/CRITICAL_ANALYSIS_HTMX_ALPINE.md), the existing implementation has:
|
|
- ✅ Basic template structure with Tailwind CSS
|
|
- ✅ Design tokens and component library documentation
|
|
- ✅ Basic HTMX integration for partial page updates
|
|
- ✅ Simple Alpine.js state management
|
|
- ✅ Dark/light mode support
|
|
- ❌ ~60-70% missing functionality vs React frontend
|
|
- ❌ Limited component reusability
|
|
- ❌ Incomplete state management patterns
|
|
- ❌ Missing advanced UI components
|
|
|
|
---
|
|
|
|
## Architecture Overview
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph Frontend Architecture
|
|
subgraph Templates Layer
|
|
BASE[base/base.html]
|
|
LAYOUTS[Layout Templates]
|
|
PAGES[Page Templates]
|
|
COMPONENTS[Component Templates]
|
|
PARTIALS[HTMX Partials]
|
|
end
|
|
|
|
subgraph Alpine.js Layer
|
|
STORES[Global Stores]
|
|
COMPONENTS_JS[Component Data]
|
|
UTILS[Utility Functions]
|
|
end
|
|
|
|
subgraph HTMX Layer
|
|
TRIGGERS[Event Triggers]
|
|
TARGETS[Swap Targets]
|
|
INDICATORS[Loading States]
|
|
end
|
|
|
|
subgraph Design System
|
|
TOKENS[Design Tokens CSS]
|
|
TAILWIND[Tailwind Config]
|
|
SHADCN[Shadcn UI Components]
|
|
end
|
|
end
|
|
|
|
BASE --> LAYOUTS
|
|
LAYOUTS --> PAGES
|
|
PAGES --> COMPONENTS
|
|
COMPONENTS --> PARTIALS
|
|
|
|
STORES --> COMPONENTS_JS
|
|
COMPONENTS_JS --> UTILS
|
|
|
|
TOKENS --> TAILWIND
|
|
TAILWIND --> SHADCN
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 1: Foundation & Infrastructure
|
|
|
|
### 1.1 Design System Enhancement
|
|
|
|
#### Design Tokens Consolidation
|
|
- Merge existing CSS variables from [base.html](../templates/base/base.html:41) with [design-tokens.md](../memory-bank/design-system/design-tokens.md)
|
|
- Create unified CSS custom properties file: `static/css/design-tokens.css`
|
|
- Implement CSS layers for better cascade management
|
|
|
|
```css
|
|
/* Proposed CSS Layer Structure */
|
|
@layer reset, tokens, base, components, utilities, overrides;
|
|
```
|
|
|
|
#### Tailwind CSS 4 Migration
|
|
- Update [tailwind.config.js](../tailwind.config.js) for Tailwind CSS 4 compatibility
|
|
- Extend color palette with design token references
|
|
- Add HTMX state variants (existing htmx-settling, htmx-request, etc.)
|
|
- Configure container queries for component-level responsiveness
|
|
|
|
#### Shadcn UI Integration
|
|
Create Django template versions of key Shadcn UI components:
|
|
- [ ] Button variants (primary, secondary, outline, ghost, destructive)
|
|
- [ ] Card component with header, body, footer slots
|
|
- [ ] Dialog/Modal with focus trapping
|
|
- [ ] Dropdown Menu with keyboard navigation
|
|
- [ ] Form components (Input, Select, Checkbox, Radio, Switch)
|
|
- [ ] Toast/Notification system
|
|
- [ ] Skeleton loading components
|
|
- [ ] Avatar with fallback
|
|
- [ ] Badge variants
|
|
- [ ] Tabs component
|
|
- [ ] Accordion/Collapsible
|
|
- [ ] Command palette (for advanced search)
|
|
|
|
### 1.2 Template Architecture Restructure
|
|
|
|
#### Proposed Directory Structure
|
|
```
|
|
templates/
|
|
├── base/
|
|
│ ├── base.html # Root template with design system
|
|
│ └── htmx-base.html # HTMX-specific base for partials
|
|
├── layouts/
|
|
│ ├── default.html # Standard page layout
|
|
│ ├── dashboard.html # User dashboard layout
|
|
│ ├── auth.html # Authentication pages layout
|
|
│ ├── sidebar.html # Sidebar navigation layout
|
|
│ └── full-width.html # Full-width content layout
|
|
├── components/
|
|
│ ├── ui/ # Shadcn-style UI components
|
|
│ │ ├── button.html
|
|
│ │ ├── card.html
|
|
│ │ ├── dialog.html
|
|
│ │ ├── dropdown.html
|
|
│ │ ├── form/
|
|
│ │ │ ├── input.html
|
|
│ │ │ ├── select.html
|
|
│ │ │ ├── checkbox.html
|
|
│ │ │ └── textarea.html
|
|
│ │ ├── toast.html
|
|
│ │ ├── skeleton.html
|
|
│ │ └── avatar.html
|
|
│ ├── navigation/
|
|
│ │ ├── navbar.html
|
|
│ │ ├── mobile-menu.html
|
|
│ │ ├── breadcrumbs.html
|
|
│ │ ├── pagination.html
|
|
│ │ └── user-menu.html
|
|
│ ├── search/
|
|
│ │ ├── search-bar.html
|
|
│ │ ├── search-results.html
|
|
│ │ ├── autocomplete.html
|
|
│ │ └── filters.html
|
|
│ ├── cards/
|
|
│ │ ├── park-card.html
|
|
│ │ ├── ride-card.html
|
|
│ │ ├── manufacturer-card.html
|
|
│ │ └── operator-card.html
|
|
│ └── data-display/
|
|
│ ├── stats-card.html
|
|
│ ├── data-table.html
|
|
│ ├── image-gallery.html
|
|
│ └── rating-display.html
|
|
├── pages/
|
|
│ ├── home/
|
|
│ │ └── homepage.html
|
|
│ ├── parks/
|
|
│ │ ├── list.html
|
|
│ │ ├── detail.html
|
|
│ │ ├── create.html
|
|
│ │ └── edit.html
|
|
│ ├── rides/
|
|
│ │ ├── list.html
|
|
│ │ ├── detail.html
|
|
│ │ ├── create.html
|
|
│ │ └── edit.html
|
|
│ ├── auth/
|
|
│ │ ├── login.html
|
|
│ │ ├── register.html
|
|
│ │ ├── forgot-password.html
|
|
│ │ └── reset-password.html
|
|
│ ├── user/
|
|
│ │ ├── profile.html
|
|
│ │ ├── settings.html
|
|
│ │ └── dashboard.html
|
|
│ └── search/
|
|
│ └── results.html
|
|
├── partials/ # HTMX swap targets
|
|
│ ├── homepage/
|
|
│ ├── parks/
|
|
│ ├── rides/
|
|
│ ├── search/
|
|
│ └── user/
|
|
└── emails/ # Email templates
|
|
```
|
|
|
|
### 1.3 Alpine.js Architecture
|
|
|
|
#### Global Store System
|
|
Create centralized Alpine.js stores in `static/js/stores/`:
|
|
|
|
```javascript
|
|
// static/js/stores/index.js
|
|
document.addEventListener('alpine:init', () => {
|
|
// Authentication Store
|
|
Alpine.store('auth', {
|
|
user: null,
|
|
isAuthenticated: false,
|
|
permissions: [],
|
|
|
|
init() {
|
|
// Initialize from server-rendered data
|
|
this.user = window.__AUTH_USER__ || null;
|
|
this.isAuthenticated = !!this.user;
|
|
},
|
|
|
|
async login(credentials) { /* ... */ },
|
|
async logout() { /* ... */ },
|
|
hasPermission(permission) { /* ... */ }
|
|
});
|
|
|
|
// Theme Store
|
|
Alpine.store('theme', {
|
|
isDark: false,
|
|
|
|
init() {
|
|
this.isDark = localStorage.getItem('theme') === 'dark' ||
|
|
(!localStorage.getItem('theme') &&
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
this.apply();
|
|
},
|
|
|
|
toggle() {
|
|
this.isDark = !this.isDark;
|
|
this.apply();
|
|
},
|
|
|
|
apply() {
|
|
document.documentElement.classList.toggle('dark', this.isDark);
|
|
localStorage.setItem('theme', this.isDark ? 'dark' : 'light');
|
|
}
|
|
});
|
|
|
|
// Search Store
|
|
Alpine.store('search', {
|
|
query: '',
|
|
results: [],
|
|
isOpen: false,
|
|
isLoading: false,
|
|
filters: {},
|
|
|
|
async search(query) { /* ... */ },
|
|
clearSearch() { /* ... */ },
|
|
applyFilters(filters) { /* ... */ }
|
|
});
|
|
|
|
// Toast/Notification Store
|
|
Alpine.store('toast', {
|
|
toasts: [],
|
|
|
|
show(message, type = 'info', duration = 5000) {
|
|
const id = Date.now();
|
|
this.toasts.push({ id, message, type, duration });
|
|
if (duration > 0) {
|
|
setTimeout(() => this.dismiss(id), duration);
|
|
}
|
|
return id;
|
|
},
|
|
|
|
dismiss(id) {
|
|
this.toasts = this.toasts.filter(t => t.id !== id);
|
|
},
|
|
|
|
success(message) { return this.show(message, 'success'); },
|
|
error(message) { return this.show(message, 'error'); },
|
|
warning(message) { return this.show(message, 'warning'); },
|
|
info(message) { return this.show(message, 'info'); }
|
|
});
|
|
|
|
// UI State Store
|
|
Alpine.store('ui', {
|
|
sidebarOpen: false,
|
|
modalStack: [],
|
|
|
|
openModal(id) { this.modalStack.push(id); },
|
|
closeModal(id) {
|
|
this.modalStack = this.modalStack.filter(m => m !== id);
|
|
},
|
|
isModalOpen(id) { return this.modalStack.includes(id); }
|
|
});
|
|
});
|
|
```
|
|
|
|
#### Reusable Component Functions
|
|
Create component-specific Alpine.js data functions:
|
|
|
|
```javascript
|
|
// static/js/components/index.js
|
|
|
|
// Modal Component
|
|
Alpine.data('modal', (config = {}) => ({
|
|
isOpen: false,
|
|
modalId: config.id || 'default',
|
|
closeOnBackdrop: config.closeOnBackdrop !== false,
|
|
closeOnEscape: config.closeOnEscape !== false,
|
|
|
|
init() {
|
|
if (this.closeOnEscape) {
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && this.isOpen) this.close();
|
|
});
|
|
}
|
|
},
|
|
|
|
open() {
|
|
this.isOpen = true;
|
|
document.body.style.overflow = 'hidden';
|
|
this.$dispatch('modal-opened', { id: this.modalId });
|
|
},
|
|
|
|
close() {
|
|
this.isOpen = false;
|
|
document.body.style.overflow = '';
|
|
this.$dispatch('modal-closed', { id: this.modalId });
|
|
},
|
|
|
|
toggle() {
|
|
this.isOpen ? this.close() : this.open();
|
|
}
|
|
}));
|
|
|
|
// Dropdown Component
|
|
Alpine.data('dropdown', (config = {}) => ({
|
|
isOpen: false,
|
|
placement: config.placement || 'bottom-start',
|
|
|
|
toggle() { this.isOpen = !this.isOpen; },
|
|
close() { this.isOpen = false; },
|
|
|
|
// Keyboard navigation
|
|
handleKeydown(event) {
|
|
if (event.key === 'ArrowDown') { /* ... */ }
|
|
if (event.key === 'ArrowUp') { /* ... */ }
|
|
if (event.key === 'Enter') { /* ... */ }
|
|
}
|
|
}));
|
|
|
|
// Search with Autocomplete
|
|
Alpine.data('searchAutocomplete', (config = {}) => ({
|
|
query: '',
|
|
results: [],
|
|
isLoading: false,
|
|
selectedIndex: -1,
|
|
minChars: config.minChars || 2,
|
|
debounceMs: config.debounceMs || 300,
|
|
endpoint: config.endpoint || '/api/v1/search/',
|
|
|
|
async search() {
|
|
if (this.query.length < this.minChars) {
|
|
this.results = [];
|
|
return;
|
|
}
|
|
|
|
this.isLoading = true;
|
|
// Debounced search implementation
|
|
},
|
|
|
|
selectResult(index) { /* ... */ },
|
|
handleKeydown(event) { /* ... */ },
|
|
clear() { /* ... */ }
|
|
}));
|
|
|
|
// Form Validation
|
|
Alpine.data('formValidation', (rules = {}) => ({
|
|
errors: {},
|
|
touched: {},
|
|
isSubmitting: false,
|
|
|
|
validate(field, value) { /* ... */ },
|
|
validateAll() { /* ... */ },
|
|
isValid() { return Object.keys(this.errors).length === 0; },
|
|
hasError(field) { return this.touched[field] && this.errors[field]; },
|
|
reset() { /* ... */ }
|
|
}));
|
|
|
|
// Tabs Component
|
|
Alpine.data('tabs', (config = {}) => ({
|
|
activeTab: config.defaultTab || 0,
|
|
|
|
setActiveTab(index) {
|
|
this.activeTab = index;
|
|
this.$dispatch('tab-changed', { index });
|
|
},
|
|
|
|
isActive(index) {
|
|
return this.activeTab === index;
|
|
}
|
|
}));
|
|
|
|
// Image Gallery with Lightbox
|
|
Alpine.data('imageGallery', () => ({
|
|
images: [],
|
|
currentIndex: 0,
|
|
isLightboxOpen: false,
|
|
|
|
openLightbox(index) { /* ... */ },
|
|
closeLightbox() { /* ... */ },
|
|
next() { /* ... */ },
|
|
prev() { /* ... */ }
|
|
}));
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: Core Component Implementation
|
|
|
|
### 2.1 Navigation Components
|
|
|
|
#### Enhanced Navbar
|
|
- Sticky header with backdrop blur
|
|
- Responsive design with mobile drawer
|
|
- User authentication state handling
|
|
- Search integration with command palette (Cmd/Ctrl + K)
|
|
- Theme toggle with system preference detection
|
|
- Active route highlighting
|
|
|
|
#### Mobile Navigation
|
|
- Slide-in drawer with gesture support
|
|
- Focus trapping for accessibility
|
|
- Smooth animations with Alpine.js transitions
|
|
- Hierarchical menu support
|
|
|
|
#### Breadcrumbs
|
|
- Automatic generation from URL structure
|
|
- Schema.org markup for SEO
|
|
- Mobile-friendly truncation
|
|
|
|
### 2.2 Card Components
|
|
|
|
#### Park Card
|
|
```html
|
|
<!-- Template: components/cards/park-card.html -->
|
|
{% load static %}
|
|
|
|
<article class="card card-hover group"
|
|
x-data="{ isHovered: false }"
|
|
@mouseenter="isHovered = true"
|
|
@mouseleave="isHovered = false">
|
|
|
|
<!-- Image with overlay -->
|
|
<div class="card-image">
|
|
<img src="{{ park.image_url|default:'/static/images/placeholder-park.jpg' }}"
|
|
alt="{{ park.name }}"
|
|
class="card-image-img"
|
|
loading="lazy">
|
|
|
|
<!-- Status Badge -->
|
|
<div class="card-image-overlay">
|
|
<span class="badge badge-{{ park.status }}">
|
|
{{ park.get_status_display }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Hover Actions -->
|
|
<div x-show="isHovered"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
class="absolute inset-0 flex items-end p-4 bg-gradient-to-t from-black/60 to-transparent">
|
|
<div class="flex gap-2">
|
|
<button class="btn btn-sm btn-secondary">
|
|
{% include 'components/icons/heart.html' %}
|
|
</button>
|
|
<button class="btn btn-sm btn-secondary">
|
|
{% include 'components/icons/share.html' %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="card-body">
|
|
<h3 class="card-title">
|
|
<a href="{{ park.get_absolute_url }}" class="card-title-link">
|
|
{{ park.name }}
|
|
</a>
|
|
</h3>
|
|
|
|
<p class="card-text">{{ park.description|truncatewords:25 }}</p>
|
|
|
|
<!-- Meta Information -->
|
|
<div class="card-meta">
|
|
<span class="card-meta-item">
|
|
{% include 'components/icons/location.html' with class='icon-xs' %}
|
|
{{ park.location.city }}, {{ park.location.country }}
|
|
</span>
|
|
<span class="card-meta-item">
|
|
{% include 'components/icons/ride.html' with class='icon-xs' %}
|
|
{{ park.rides_count }} rides
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Rating -->
|
|
{% if park.average_rating %}
|
|
<div class="flex items-center gap-2 mt-3">
|
|
{% include 'components/data-display/rating-stars.html' with rating=park.average_rating %}
|
|
<span class="text-sm text-muted-foreground">
|
|
({{ park.review_count }} reviews)
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</article>
|
|
```
|
|
|
|
#### Ride Card
|
|
- Similar structure with ride-specific data
|
|
- Category/type badges
|
|
- Height/intensity indicators
|
|
- Manufacturer attribution
|
|
|
|
### 2.3 Form Components
|
|
|
|
#### Enhanced Input Component
|
|
```html
|
|
<!-- Template: components/ui/form/input.html -->
|
|
{% load widget_tweaks %}
|
|
|
|
<div class="form-group"
|
|
x-data="{ focused: false, hasValue: {{ field.value|yesno:'true,false' }} }">
|
|
|
|
{% if label %}
|
|
<label for="{{ field.id_for_label }}"
|
|
class="form-label"
|
|
:class="{ 'text-primary': focused }">
|
|
{{ label }}
|
|
{% if field.field.required %}
|
|
<span class="text-error-500" aria-label="required">*</span>
|
|
{% endif %}
|
|
</label>
|
|
{% endif %}
|
|
|
|
<div class="relative">
|
|
{% if icon_left %}
|
|
<div class="absolute -translate-y-1/2 left-3 top-1/2 text-muted-foreground">
|
|
{{ icon_left }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{{ field|add_class:"form-input"|attr:"x-on:focus=focused = true"|attr:"x-on:blur=focused = false; hasValue = $el.value.length > 0" }}
|
|
|
|
{% if icon_right %}
|
|
<div class="absolute -translate-y-1/2 right-3 top-1/2 text-muted-foreground">
|
|
{{ icon_right }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if field.help_text %}
|
|
<p class="form-help">{{ field.help_text }}</p>
|
|
{% endif %}
|
|
|
|
{% if field.errors %}
|
|
<div class="form-error" role="alert">
|
|
{{ field.errors.0 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
```
|
|
|
|
### 2.4 Modal/Dialog System
|
|
|
|
#### HTMX-Powered Modal
|
|
```html
|
|
<!-- Template: components/ui/dialog.html -->
|
|
<div x-data="modal({ id: '{{ modal_id }}', closeOnBackdrop: {{ close_on_backdrop|default:'true' }} })"
|
|
x-show="isOpen"
|
|
x-on:open-modal.window="if ($event.detail.id === modalId) open()"
|
|
x-on:close-modal.window="if ($event.detail.id === modalId) close()"
|
|
class="modal-overlay"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="modal-title-{{ modal_id }}">
|
|
|
|
<!-- Backdrop -->
|
|
<div class="modal-backdrop"
|
|
@click="closeOnBackdrop && close()"
|
|
aria-hidden="true"></div>
|
|
|
|
<!-- Modal Content -->
|
|
<div class="modal-container"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95">
|
|
|
|
<div class="modal-content">
|
|
{% if show_header %}
|
|
<div class="modal-header">
|
|
<h2 id="modal-title-{{ modal_id }}" class="modal-title">
|
|
{{ title }}
|
|
</h2>
|
|
<button @click="close()"
|
|
class="btn-icon"
|
|
aria-label="Close modal">
|
|
{% include 'components/icons/x.html' %}
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="modal-body"
|
|
id="modal-body-{{ modal_id }}"
|
|
hx-target="this"
|
|
hx-swap="innerHTML">
|
|
{% block modal_content %}{% endblock %}
|
|
</div>
|
|
|
|
{% if show_footer %}
|
|
<div class="modal-footer">
|
|
{% block modal_footer %}
|
|
<button @click="close()" class="btn btn-secondary">Cancel</button>
|
|
<button class="btn btn-primary">Confirm</button>
|
|
{% endblock %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### 2.5 Toast/Notification System
|
|
|
|
```html
|
|
<!-- Template: components/ui/toast-container.html -->
|
|
<div x-data
|
|
class="fixed z-50 flex flex-col max-w-sm gap-2 bottom-4 right-4">
|
|
|
|
<template x-for="toast in $store.toast.toasts" :key="toast.id">
|
|
<div x-show="true"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 translate-y-2"
|
|
x-transition:enter-end="opacity-100 translate-y-0"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0 translate-x-full"
|
|
:class="{
|
|
'bg-success-50 border-success-200 text-success-800': toast.type === 'success',
|
|
'bg-error-50 border-error-200 text-error-800': toast.type === 'error',
|
|
'bg-warning-50 border-warning-200 text-warning-800': toast.type === 'warning',
|
|
'bg-info-50 border-info-200 text-info-800': toast.type === 'info'
|
|
}"
|
|
class="flex items-start gap-3 p-4 border rounded-lg shadow-lg">
|
|
|
|
<!-- Icon -->
|
|
<div class="flex-shrink-0">
|
|
<template x-if="toast.type === 'success'">
|
|
{% include 'components/icons/check-circle.html' %}
|
|
</template>
|
|
<template x-if="toast.type === 'error'">
|
|
{% include 'components/icons/x-circle.html' %}
|
|
</template>
|
|
<!-- ... other icons -->
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1">
|
|
<p x-text="toast.message" class="text-sm font-medium"></p>
|
|
</div>
|
|
|
|
<!-- Dismiss -->
|
|
<button @click="$store.toast.dismiss(toast.id)"
|
|
class="flex-shrink-0 p-1 transition-colors rounded hover:bg-black/10">
|
|
{% include 'components/icons/x.html' with class='w-4 h-4' %}
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 3: Feature Implementation
|
|
|
|
### 3.1 Authentication System
|
|
|
|
#### Modal-Based Authentication
|
|
- Login modal with form validation
|
|
- Registration with multi-step wizard
|
|
- Password reset flow
|
|
- Social authentication integration
|
|
- Remember me functionality
|
|
- Session management with HTMX
|
|
|
|
#### User Profile & Settings
|
|
- Profile editing with image upload
|
|
- Account settings management
|
|
- Notification preferences
|
|
- Privacy settings
|
|
- Connected accounts
|
|
|
|
### 3.2 Advanced Search System
|
|
|
|
#### Global Search with Command Palette
|
|
```html
|
|
<!-- Cmd/Ctrl + K activated search -->
|
|
<div x-data="commandPalette()"
|
|
@keydown.window.cmd.k.prevent="open()"
|
|
@keydown.window.ctrl.k.prevent="open()">
|
|
|
|
<div x-show="isOpen"
|
|
class="command-palette-overlay"
|
|
x-transition>
|
|
|
|
<div class="command-palette">
|
|
<div class="command-palette-input-wrapper">
|
|
<input type="text"
|
|
x-model="query"
|
|
@input.debounce.200ms="search()"
|
|
@keydown.arrow-down.prevent="navigateDown()"
|
|
@keydown.arrow-up.prevent="navigateUp()"
|
|
@keydown.enter.prevent="selectCurrent()"
|
|
@keydown.escape="close()"
|
|
placeholder="Search parks, rides, users..."
|
|
class="command-palette-input"
|
|
x-ref="input">
|
|
</div>
|
|
|
|
<div class="command-palette-results">
|
|
<!-- Results grouped by category -->
|
|
<template x-for="(group, groupName) in groupedResults">
|
|
<div class="command-palette-group">
|
|
<div class="command-palette-group-header" x-text="groupName"></div>
|
|
<template x-for="(result, index) in group">
|
|
<a :href="result.url"
|
|
class="command-palette-item"
|
|
:class="{ 'active': isSelected(result) }">
|
|
<!-- Result item -->
|
|
</a>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
#### Filter System with URL Sync
|
|
- Multi-select filters
|
|
- Range sliders for numeric values
|
|
- Location-based filtering
|
|
- Real-time result updates via HTMX
|
|
- URL state synchronization for shareability
|
|
|
|
### 3.3 Data Tables
|
|
|
|
#### Advanced Data Table Component
|
|
- Sortable columns
|
|
- Pagination with HTMX
|
|
- Row selection
|
|
- Bulk actions
|
|
- Column visibility toggle
|
|
- Export functionality
|
|
- Responsive design with horizontal scroll
|
|
|
|
### 3.4 Image Gallery & Media
|
|
|
|
#### Image Gallery with Lightbox
|
|
- Lazy loading with intersection observer
|
|
- Touch gestures for mobile
|
|
- Keyboard navigation
|
|
- Zoom functionality
|
|
- Image upload with drag & drop
|
|
- Progress indicators
|
|
|
|
---
|
|
|
|
## Phase 4: HTMX Integration Patterns
|
|
|
|
### 4.1 Standard Patterns
|
|
|
|
#### Partial Updates
|
|
```html
|
|
<!-- Trigger partial update -->
|
|
<button hx-get="/api/v1/parks/{{ park.id }}/details/"
|
|
hx-target="#park-details"
|
|
hx-swap="innerHTML"
|
|
hx-indicator="#loading-spinner"
|
|
class="btn btn-primary">
|
|
Load Details
|
|
</button>
|
|
|
|
<div id="park-details">
|
|
<!-- Content loaded here -->
|
|
</div>
|
|
|
|
<div id="loading-spinner" class="htmx-indicator">
|
|
{% include 'components/ui/skeleton.html' %}
|
|
</div>
|
|
```
|
|
|
|
#### Infinite Scroll
|
|
```html
|
|
<div id="park-list"
|
|
hx-get="/api/v1/parks/?page={{ next_page }}"
|
|
hx-trigger="revealed"
|
|
hx-swap="beforeend"
|
|
hx-indicator="#infinite-loader">
|
|
|
|
{% for park in parks %}
|
|
{% include 'components/cards/park-card.html' %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div id="infinite-loader" class="py-8 text-center htmx-indicator">
|
|
<div class="inline-block w-8 h-8 border-2 rounded-full animate-spin border-primary border-t-transparent"></div>
|
|
</div>
|
|
```
|
|
|
|
#### Optimistic Updates
|
|
```html
|
|
<button hx-post="/api/v1/parks/{{ park.id }}/favorite/"
|
|
hx-swap="outerHTML"
|
|
hx-on::before-request="this.classList.add('is-favorited')"
|
|
hx-on::response-error="this.classList.remove('is-favorited')"
|
|
class="btn-icon favorite-btn">
|
|
{% include 'components/icons/heart.html' %}
|
|
</button>
|
|
```
|
|
|
|
### 4.2 Error Handling
|
|
|
|
```javascript
|
|
// Global HTMX error handling
|
|
document.addEventListener('htmx:responseError', (event) => {
|
|
const status = event.detail.xhr.status;
|
|
|
|
if (status === 401) {
|
|
Alpine.store('toast').error('Please log in to continue');
|
|
// Optionally open login modal
|
|
} else if (status === 403) {
|
|
Alpine.store('toast').error('You do not have permission to perform this action');
|
|
} else if (status === 422) {
|
|
// Validation errors - handled by individual forms
|
|
} else if (status >= 500) {
|
|
Alpine.store('toast').error('Something went wrong. Please try again.');
|
|
}
|
|
});
|
|
|
|
// Loading state management
|
|
document.addEventListener('htmx:beforeRequest', (event) => {
|
|
event.target.classList.add('is-loading');
|
|
});
|
|
|
|
document.addEventListener('htmx:afterRequest', (event) => {
|
|
event.target.classList.remove('is-loading');
|
|
});
|
|
```
|
|
|
|
### 4.3 HX-Trigger Events
|
|
|
|
```python
|
|
# Backend response with HX-Trigger
|
|
from django.http import HttpResponse
|
|
|
|
def favorite_park(request, pk):
|
|
# ... logic
|
|
response = HttpResponse(render_to_string('partials/favorite-button.html', {...}))
|
|
response['HX-Trigger'] = json.dumps({
|
|
'showToast': {'message': 'Park added to favorites', 'type': 'success'},
|
|
'updateFavoritesCount': {'count': new_count}
|
|
})
|
|
return response
|
|
```
|
|
|
|
```javascript
|
|
// Listen for custom HX-Trigger events
|
|
document.addEventListener('showToast', (event) => {
|
|
const { message, type } = event.detail;
|
|
Alpine.store('toast').show(message, type);
|
|
});
|
|
|
|
document.addEventListener('updateFavoritesCount', (event) => {
|
|
const { count } = event.detail;
|
|
document.querySelector('#favorites-count').textContent = count;
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: Performance & Optimization
|
|
|
|
### 5.1 Asset Optimization
|
|
|
|
- CSS minification and purging
|
|
- JavaScript bundling with proper chunking
|
|
- Image optimization with WebP/AVIF support
|
|
- Lazy loading for below-fold content
|
|
- Preloading critical assets
|
|
|
|
### 5.2 Caching Strategy
|
|
|
|
```html
|
|
<!-- Cache partial responses -->
|
|
<div hx-get="/api/v1/parks/featured/"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML"
|
|
hx-headers='{"X-Cache-Control": "max-age=300"}'>
|
|
</div>
|
|
```
|
|
|
|
### 5.3 Performance Monitoring
|
|
|
|
- Core Web Vitals tracking
|
|
- HTMX request timing
|
|
- Alpine.js component performance
|
|
- Error tracking and reporting
|
|
|
|
---
|
|
|
|
## Phase 6: Testing Strategy
|
|
|
|
### 6.1 Unit Testing
|
|
|
|
- Alpine.js store functions
|
|
- Utility JavaScript functions
|
|
- Django template tags
|
|
|
|
### 6.2 Integration Testing
|
|
|
|
- HTMX endpoint responses
|
|
- Component rendering
|
|
- Form submissions
|
|
- Authentication flows
|
|
|
|
### 6.3 E2E Testing
|
|
|
|
- User journeys with Playwright
|
|
- Visual regression testing
|
|
- Accessibility testing (axe-core)
|
|
- Cross-browser testing
|
|
|
|
### 6.4 Accessibility Testing
|
|
|
|
- WCAG 2.1 AA compliance
|
|
- Screen reader compatibility
|
|
- Keyboard navigation
|
|
- Color contrast verification
|
|
- Focus management
|
|
|
|
---
|
|
|
|
## Implementation Phases & Milestones
|
|
|
|
### Phase 1: Foundation (Week 1-2)
|
|
- [ ] Design system CSS consolidation
|
|
- [ ] Tailwind CSS 4 configuration
|
|
- [ ] Template directory restructure
|
|
- [ ] Alpine.js store architecture
|
|
- [ ] Base component implementation
|
|
|
|
### Phase 2: Core Components (Week 3-4)
|
|
- [ ] Navigation components
|
|
- [ ] Card components (park, ride, etc.)
|
|
- [ ] Form components
|
|
- [ ] Modal/Dialog system
|
|
- [ ] Toast notifications
|
|
|
|
### Phase 3: Feature Pages (Week 5-6)
|
|
- [ ] Homepage redesign
|
|
- [ ] Park list/detail pages
|
|
- [ ] Ride list/detail pages
|
|
- [ ] Search results page
|
|
- [ ] User profile/settings
|
|
|
|
### Phase 4: Advanced Features (Week 7-8)
|
|
- [ ] Authentication system
|
|
- [ ] Advanced search/filters
|
|
- [ ] Data tables
|
|
- [ ] Image gallery
|
|
- [ ] Real-time features
|
|
|
|
### Phase 5: Polish & Testing (Week 9-10)
|
|
- [ ] Performance optimization
|
|
- [ ] Accessibility audit
|
|
- [ ] Cross-browser testing
|
|
- [ ] Documentation
|
|
- [ ] Bug fixes
|
|
|
|
---
|
|
|
|
## File Dependencies & Migration Order
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[Design Tokens CSS] --> B[Tailwind Config]
|
|
B --> C[Base Template]
|
|
C --> D[Layout Templates]
|
|
D --> E[UI Components]
|
|
E --> F[Page Templates]
|
|
F --> G[Partials]
|
|
|
|
H[Alpine.js Stores] --> I[Component Data Functions]
|
|
I --> J[Page-specific JS]
|
|
|
|
K[HTMX Config] --> L[Endpoint Integration]
|
|
L --> M[Error Handling]
|
|
```
|
|
|
|
---
|
|
|
|
## Risk Mitigation
|
|
|
|
| Risk | Mitigation Strategy |
|
|
|------|---------------------|
|
|
| Breaking existing functionality | Implement feature flags, incremental rollout |
|
|
| Performance regression | Continuous performance monitoring, benchmarking |
|
|
| Accessibility issues | Automated testing with axe-core, manual audits |
|
|
| Browser compatibility | Cross-browser testing matrix, progressive enhancement |
|
|
| State management complexity | Clear documentation, consistent patterns |
|
|
|
|
---
|
|
|
|
## Success Metrics
|
|
|
|
- **Performance**: LCP < 2.5s, FID < 100ms, CLS < 0.1
|
|
- **Accessibility**: WCAG 2.1 AA compliance
|
|
- **Code Quality**: 80%+ test coverage, no critical linting errors
|
|
- **Developer Experience**: Component reuse > 70%, clear documentation
|
|
- **User Experience**: Consistent interactions, smooth animations
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. Review and approve this plan
|
|
2. Set up development environment with new structure
|
|
3. Begin Phase 1 implementation
|
|
4. Schedule weekly progress reviews
|
|
|
|
---
|
|
|
|
*This plan is a living document and will be updated as implementation progresses.*
|