{{ park.name }}
{{ park.description|truncatewords:25 }}
{% if park.average_rating %}# 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
{% load static %}
{{ park.description|truncatewords:25 }}
{{ park.name }}
{{ field.help_text }}
{% endif %} {% if field.errors %}