forked from
smokesignal.events/smokesignal
The smokesignal.events web application
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}