/** * Main entry point for Smokesignal frontend * * This file initializes all core functionality that runs on every page. * Heavy libraries (maps, cropper) are lazy-loaded only when needed. * * The SmokesignalApp global provides methods that can be called multiple times * safely from both initial page load and htmx-swapped content. */ // Import styles (Vite will bundle into site.css) import '../styles/main.css' // Core libraries - htmx, extensions, and Alpine (loaded on every page) import './core' // Core utilities import { initNavigation } from './components/navigation' // Auth utilities for session management import { AuthRequiredError, authFetch, authPostJson, refreshAuth, refreshSession, SessionExpiredError, } from './core/auth' import { eventForm } from './features/events/create' import { initQuickEventForm } from './features/events/quick-create' // Features - Alpine.js components for page-specific functionality import { lfgForm } from './features/lfg/form' /** * Lazy loading state tracking * Ensures each library is only loaded once even if init is called multiple times */ const loadingState = { maps: null as Promise | null, cropper: null as Promise | null, profileCropper: null as Promise | null, lfgHeatmap: null as Promise | null, } /** * Initialize the event map if the container exists * Safe to call multiple times - will only initialize once per container */ async function initEventMap(): Promise { if (!document.getElementById('event-map')) return if (!loadingState.maps) { loadingState.maps = import('./features/maps') } const { initEventMap } = await loadingState.maps await initEventMap() } /** * Initialize the globe map if the container exists * Safe to call multiple times - will only initialize once per container */ async function initGlobeMap(): Promise { if (!document.getElementById('globe-map')) return if (!loadingState.maps) { loadingState.maps = import('./features/maps') } const { initGlobeMap } = await loadingState.maps await initGlobeMap() } /** * Initialize the location heatmap if the container exists * Safe to call multiple times - will only initialize once per container */ async function initLocationHeatmap(): Promise { if (!document.getElementById('location-heatmap')) return if (!loadingState.maps) { loadingState.maps = import('./features/maps') } const { initLocationHeatmap } = await loadingState.maps await initLocationHeatmap() } /** * Initialize all maps - convenience method for pages with multiple map types * Safe to call multiple times */ async function initMaps(): Promise { await Promise.all([initEventMap(), initGlobeMap(), initLocationHeatmap()]) } /** * Initialize the image cropper if canvas containers exist * Safe to call multiple times - will only initialize once per container */ async function initCropper(): Promise { if (!document.getElementById('headerCanvas') && !document.getElementById('thumbnailCanvas')) { return } if (!loadingState.cropper) { loadingState.cropper = import('./features/cropper') } const { initCropper } = await loadingState.cropper await initCropper() } /** * Initialize the profile image cropper if avatar/banner elements exist * Safe to call multiple times - will only initialize once */ async function initProfileCropper(): Promise { if (!document.getElementById('avatar-input') && !document.getElementById('banner-input')) { return } if (!loadingState.profileCropper) { loadingState.profileCropper = import('./features/cropper/profile-cropper') } const { initProfileCropper } = await loadingState.profileCropper await initProfileCropper() } /** * Initialize the LFG heatmap if its container exists * Safe to call multiple times - will only initialize once per container */ async function initLfgHeatmap(): Promise { if (!document.getElementById('lfg-heatmap')) return if (!loadingState.lfgHeatmap) { loadingState.lfgHeatmap = import('./features/lfg/heatmap') } const { initLfgHeatmap } = await loadingState.lfgHeatmap await initLfgHeatmap() } /** * Initialize all page functionality * Called on DOMContentLoaded and can be called after htmx swaps */ function initPage(): void { // Components that run on every page initNavigation() // Initialize quick event form (lightweight, no lazy loading needed) initQuickEventForm() // Auto-initialize maps if containers exist initMaps() // Auto-initialize cropper if containers exist initCropper() // Auto-initialize profile cropper if containers exist initProfileCropper() // Auto-initialize LFG heatmap if container exists initLfgHeatmap() } // Initialize on DOMContentLoaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initPage) } else { initPage() } // Re-initialize after HTMX swaps (for SPA-like navigation) document.body.addEventListener('htmx:afterSettle', () => { // Re-check for new containers after HTMX content swap initMaps() initCropper() initProfileCropper() initLfgHeatmap() }) /** * SmokesignalApp - Global namespace for template integration * * Provides: * - Alpine.js component factories (lfgForm, eventForm) * - Lazy-loading initializers (initEventMap, initGlobeMap, etc.) * - Auth utilities (refreshSession, authFetch, authPostJson) * * Usage in templates: *
...
* * */ const SmokesignalApp = { // Alpine.js component factories lfgForm, eventForm, // Lazy-loading initializers (safe to call multiple times) initEventMap, initGlobeMap, initLocationHeatmap, initMaps, initCropper, initProfileCropper, initLfgHeatmap, // Re-initialize page (useful after major DOM changes) initPage, // Auth utilities for session management refreshSession, refreshAuth, authFetch, authPostJson, SessionExpiredError, AuthRequiredError, } // Expose SmokesignalApp namespace for inline scripts declare global { interface Window { SmokesignalApp: typeof SmokesignalApp } } window.SmokesignalApp = SmokesignalApp // Re-export for ES module consumers export { lfgForm, eventForm, initEventMap, initGlobeMap, initLocationHeatmap, initMaps, initCropper, initProfileCropper, initLfgHeatmap, initPage, // Auth utilities refreshSession, refreshAuth, authFetch, authPostJson, SessionExpiredError, AuthRequiredError, }