The smokesignal.events web application
at main 247 lines 6.8 kB view raw
1/** 2 * Main entry point for Smokesignal frontend 3 * 4 * This file initializes all core functionality that runs on every page. 5 * Heavy libraries (maps, cropper) are lazy-loaded only when needed. 6 * 7 * The SmokesignalApp global provides methods that can be called multiple times 8 * safely from both initial page load and htmx-swapped content. 9 */ 10 11// Import styles (Vite will bundle into site.css) 12import '../styles/main.css' 13 14// Core libraries - htmx, extensions, and Alpine (loaded on every page) 15import './core' 16 17// Core utilities 18import { initNavigation } from './components/navigation' 19// Auth utilities for session management 20import { 21 AuthRequiredError, 22 authFetch, 23 authPostJson, 24 refreshAuth, 25 refreshSession, 26 SessionExpiredError, 27} from './core/auth' 28import { eventForm } from './features/events/create' 29import { initQuickEventForm } from './features/events/quick-create' 30// Features - Alpine.js components for page-specific functionality 31import { lfgForm } from './features/lfg/form' 32 33/** 34 * Lazy loading state tracking 35 * Ensures each library is only loaded once even if init is called multiple times 36 */ 37const loadingState = { 38 maps: null as Promise<typeof import('./features/maps')> | null, 39 cropper: null as Promise<typeof import('./features/cropper')> | null, 40 profileCropper: null as Promise<typeof import('./features/cropper/profile-cropper')> | null, 41 lfgHeatmap: null as Promise<typeof import('./features/lfg/heatmap')> | null, 42} 43 44/** 45 * Initialize the event map if the container exists 46 * Safe to call multiple times - will only initialize once per container 47 */ 48async function initEventMap(): Promise<void> { 49 if (!document.getElementById('event-map')) return 50 51 if (!loadingState.maps) { 52 loadingState.maps = import('./features/maps') 53 } 54 const { initEventMap } = await loadingState.maps 55 await initEventMap() 56} 57 58/** 59 * Initialize the globe map if the container exists 60 * Safe to call multiple times - will only initialize once per container 61 */ 62async function initGlobeMap(): Promise<void> { 63 if (!document.getElementById('globe-map')) return 64 65 if (!loadingState.maps) { 66 loadingState.maps = import('./features/maps') 67 } 68 const { initGlobeMap } = await loadingState.maps 69 await initGlobeMap() 70} 71 72/** 73 * Initialize the location heatmap if the container exists 74 * Safe to call multiple times - will only initialize once per container 75 */ 76async function initLocationHeatmap(): Promise<void> { 77 if (!document.getElementById('location-heatmap')) return 78 79 if (!loadingState.maps) { 80 loadingState.maps = import('./features/maps') 81 } 82 const { initLocationHeatmap } = await loadingState.maps 83 await initLocationHeatmap() 84} 85 86/** 87 * Initialize all maps - convenience method for pages with multiple map types 88 * Safe to call multiple times 89 */ 90async function initMaps(): Promise<void> { 91 await Promise.all([initEventMap(), initGlobeMap(), initLocationHeatmap()]) 92} 93 94/** 95 * Initialize the image cropper if canvas containers exist 96 * Safe to call multiple times - will only initialize once per container 97 */ 98async function initCropper(): Promise<void> { 99 if (!document.getElementById('headerCanvas') && !document.getElementById('thumbnailCanvas')) { 100 return 101 } 102 103 if (!loadingState.cropper) { 104 loadingState.cropper = import('./features/cropper') 105 } 106 const { initCropper } = await loadingState.cropper 107 await initCropper() 108} 109 110/** 111 * Initialize the profile image cropper if avatar/banner elements exist 112 * Safe to call multiple times - will only initialize once 113 */ 114async function initProfileCropper(): Promise<void> { 115 if (!document.getElementById('avatar-input') && !document.getElementById('banner-input')) { 116 return 117 } 118 119 if (!loadingState.profileCropper) { 120 loadingState.profileCropper = import('./features/cropper/profile-cropper') 121 } 122 const { initProfileCropper } = await loadingState.profileCropper 123 await initProfileCropper() 124} 125 126/** 127 * Initialize the LFG heatmap if its container exists 128 * Safe to call multiple times - will only initialize once per container 129 */ 130async function initLfgHeatmap(): Promise<void> { 131 if (!document.getElementById('lfg-heatmap')) return 132 133 if (!loadingState.lfgHeatmap) { 134 loadingState.lfgHeatmap = import('./features/lfg/heatmap') 135 } 136 const { initLfgHeatmap } = await loadingState.lfgHeatmap 137 await initLfgHeatmap() 138} 139 140/** 141 * Initialize all page functionality 142 * Called on DOMContentLoaded and can be called after htmx swaps 143 */ 144function initPage(): void { 145 // Components that run on every page 146 initNavigation() 147 148 // Initialize quick event form (lightweight, no lazy loading needed) 149 initQuickEventForm() 150 151 // Auto-initialize maps if containers exist 152 initMaps() 153 154 // Auto-initialize cropper if containers exist 155 initCropper() 156 157 // Auto-initialize profile cropper if containers exist 158 initProfileCropper() 159 160 // Auto-initialize LFG heatmap if container exists 161 initLfgHeatmap() 162} 163 164// Initialize on DOMContentLoaded 165if (document.readyState === 'loading') { 166 document.addEventListener('DOMContentLoaded', initPage) 167} else { 168 initPage() 169} 170 171// Re-initialize after HTMX swaps (for SPA-like navigation) 172document.body.addEventListener('htmx:afterSettle', () => { 173 // Re-check for new containers after HTMX content swap 174 initMaps() 175 initCropper() 176 initProfileCropper() 177 initLfgHeatmap() 178}) 179 180/** 181 * SmokesignalApp - Global namespace for template integration 182 * 183 * Provides: 184 * - Alpine.js component factories (lfgForm, eventForm) 185 * - Lazy-loading initializers (initEventMap, initGlobeMap, etc.) 186 * - Auth utilities (refreshSession, authFetch, authPostJson) 187 * 188 * Usage in templates: 189 * <div x-data="SmokesignalApp.lfgForm()">...</div> 190 * <script>SmokesignalApp.initEventMap()</script> 191 * <script>SmokesignalApp.authPostJson('/api/endpoint', data)</script> 192 */ 193const SmokesignalApp = { 194 // Alpine.js component factories 195 lfgForm, 196 eventForm, 197 198 // Lazy-loading initializers (safe to call multiple times) 199 initEventMap, 200 initGlobeMap, 201 initLocationHeatmap, 202 initMaps, 203 initCropper, 204 initProfileCropper, 205 initLfgHeatmap, 206 207 // Re-initialize page (useful after major DOM changes) 208 initPage, 209 210 // Auth utilities for session management 211 refreshSession, 212 refreshAuth, 213 authFetch, 214 authPostJson, 215 SessionExpiredError, 216 AuthRequiredError, 217} 218 219// Expose SmokesignalApp namespace for inline scripts 220declare global { 221 interface Window { 222 SmokesignalApp: typeof SmokesignalApp 223 } 224} 225 226window.SmokesignalApp = SmokesignalApp 227 228// Re-export for ES module consumers 229export { 230 lfgForm, 231 eventForm, 232 initEventMap, 233 initGlobeMap, 234 initLocationHeatmap, 235 initMaps, 236 initCropper, 237 initProfileCropper, 238 initLfgHeatmap, 239 initPage, 240 // Auth utilities 241 refreshSession, 242 refreshAuth, 243 authFetch, 244 authPostJson, 245 SessionExpiredError, 246 AuthRequiredError, 247}