mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
* fix: move foreman to global gems to fix startup crash (#1971) * Update exporting code to stream points data to file in batches to red… (#1980) * Update exporting code to stream points data to file in batches to reduce memory usage * Update changelog * Update changelog * Feature/maplibre frontend (#1953) * Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet * Implement phase 1 * Phases 1-3 + part of 4 * Fix e2e tests * Phase 6 * Implement fog of war * Phase 7 * Next step: fix specs, phase 7 done * Use our own map tiles * Extract v2 map logic to separate manager classes * Update settings panel on v2 map * Update v2 e2e tests structure * Reimplement location search in maps v2 * Update speed routes * Implement visits and places creation in v2 * Fix last failing test * Implement visits merging * Fix a routes e2e test and simplify the routes layer styling. * Extract js to modules from maps_v2_controller.js * Implement area creation * Fix spec problem * Fix some e2e tests * Implement live mode in v2 map * Update icons and panel * Extract some styles * Remove unused file * Start adding dark theme to popups on MapLibre maps * Make popups respect dark theme * Move v2 maps to maplibre namespace * Update v2 references to maplibre * Put place, area and visit info into side panel * Update API to use safe settings config method * Fix specs * Fix method name to config in SafeSettings and update usages accordingly * Add missing public files * Add handling for real time points * Fix remembering enabled/disabled layers of the v2 map * Fix lots of e2e tests * Add settings to select map version * Use maps/v2 as main path for MapLibre maps * Update routing * Update live mode * Update maplibre controller * Update changelog * Remove some console.log statements --------- Co-authored-by: Robin Tuszik <mail@robin.gg>
724 lines
17 KiB
Markdown
724 lines
17 KiB
Markdown
# Dawarich JavaScript Architecture
|
|
|
|
This document provides a comprehensive guide to the JavaScript architecture used in the Dawarich application, with a focus on the Maps (MapLibre) implementation.
|
|
|
|
## Table of Contents
|
|
|
|
- [Overview](#overview)
|
|
- [Technology Stack](#technology-stack)
|
|
- [Architecture Patterns](#architecture-patterns)
|
|
- [Directory Structure](#directory-structure)
|
|
- [Core Concepts](#core-concepts)
|
|
- [Maps (MapLibre) Architecture](#maps-maplibre-architecture)
|
|
- [Creating New Features](#creating-new-features)
|
|
- [Best Practices](#best-practices)
|
|
|
|
## Overview
|
|
|
|
Dawarich uses a modern JavaScript architecture built on **Hotwire (Turbo + Stimulus)** for page interactions and **MapLibre GL JS** for map rendering. The Maps (MapLibre) implementation follows object-oriented principles with clear separation of concerns.
|
|
|
|
## Technology Stack
|
|
|
|
- **Stimulus** - Modest JavaScript framework for sprinkles of interactivity
|
|
- **Turbo Rails** - SPA-like page navigation without building an SPA
|
|
- **MapLibre GL JS** - Open-source map rendering engine
|
|
- **ES6 Modules** - Modern JavaScript module system
|
|
- **Tailwind CSS + DaisyUI** - Utility-first CSS framework
|
|
|
|
## Architecture Patterns
|
|
|
|
### 1. Stimulus Controllers
|
|
|
|
**Purpose:** Connect DOM elements to JavaScript behavior
|
|
|
|
**Location:** `app/javascript/controllers/`
|
|
|
|
**Pattern:**
|
|
```javascript
|
|
import { Controller } from '@hotwired/stimulus'
|
|
|
|
export default class extends Controller {
|
|
static targets = ['element']
|
|
static values = { apiKey: String }
|
|
|
|
connect() {
|
|
// Initialize when element appears in DOM
|
|
}
|
|
|
|
disconnect() {
|
|
// Cleanup when element is removed
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key Principles:**
|
|
- Controllers should be stateless when possible
|
|
- Use `targets` for DOM element references
|
|
- Use `values` for passing data from HTML
|
|
- Always cleanup in `disconnect()`
|
|
|
|
### 2. Service Classes
|
|
|
|
**Purpose:** Encapsulate business logic and API communication
|
|
|
|
**Location:** `app/javascript/maps_maplibre/services/`
|
|
|
|
**Pattern:**
|
|
```javascript
|
|
export class ApiClient {
|
|
constructor(apiKey) {
|
|
this.apiKey = apiKey
|
|
}
|
|
|
|
async fetchData() {
|
|
const response = await fetch(url, {
|
|
headers: this.getHeaders()
|
|
})
|
|
return response.json()
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key Principles:**
|
|
- Single responsibility - one service per concern
|
|
- Consistent error handling
|
|
- Return promises for async operations
|
|
- Use constructor injection for dependencies
|
|
|
|
### 3. Layer Classes (Map Layers)
|
|
|
|
**Purpose:** Manage map visualization layers
|
|
|
|
**Location:** `app/javascript/maps_maplibre/layers/`
|
|
|
|
**Pattern:**
|
|
```javascript
|
|
import { BaseLayer } from './base_layer'
|
|
|
|
export class CustomLayer extends BaseLayer {
|
|
constructor(map, options = {}) {
|
|
super(map, { id: 'custom', ...options })
|
|
}
|
|
|
|
getSourceConfig() {
|
|
return {
|
|
type: 'geojson',
|
|
data: this.data
|
|
}
|
|
}
|
|
|
|
getLayerConfigs() {
|
|
return [{
|
|
id: this.id,
|
|
type: 'circle',
|
|
source: this.sourceId,
|
|
paint: { /* ... */ }
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key Principles:**
|
|
- All layers extend `BaseLayer`
|
|
- Implement `getSourceConfig()` and `getLayerConfigs()`
|
|
- Store data in `this.data`
|
|
- Use `this.visible` for visibility state
|
|
- Inherit common methods: `add()`, `update()`, `show()`, `hide()`, `toggle()`
|
|
|
|
### 4. Utility Modules
|
|
|
|
**Purpose:** Provide reusable helper functions
|
|
|
|
**Location:** `app/javascript/maps_maplibre/utils/`
|
|
|
|
**Pattern:**
|
|
```javascript
|
|
export class UtilityClass {
|
|
static helperMethod(param) {
|
|
// Static methods for stateless utilities
|
|
}
|
|
}
|
|
|
|
// Or singleton pattern
|
|
export const utilityInstance = new UtilityClass()
|
|
```
|
|
|
|
### 5. Component Classes
|
|
|
|
**Purpose:** Reusable UI components
|
|
|
|
**Location:** `app/javascript/maps_maplibre/components/`
|
|
|
|
**Pattern:**
|
|
```javascript
|
|
export class PopupFactory {
|
|
static createPopup(data) {
|
|
return `<div>${data.name}</div>`
|
|
}
|
|
}
|
|
```
|
|
|
|
## Directory Structure
|
|
|
|
```
|
|
app/javascript/
|
|
├── application.js # Entry point
|
|
├── controllers/ # Stimulus controllers
|
|
│ ├── maps/maplibre_controller.js # Main map controller
|
|
│ ├── maps_maplibre/ # Controller modules
|
|
│ │ ├── layer_manager.js # Layer lifecycle management
|
|
│ │ ├── data_loader.js # API data fetching
|
|
│ │ ├── event_handlers.js # Map event handling
|
|
│ │ ├── filter_manager.js # Data filtering
|
|
│ │ └── date_manager.js # Date range management
|
|
│ └── ... # Other controllers
|
|
├── maps_maplibre/ # Maps (MapLibre) implementation
|
|
│ ├── layers/ # Map layer classes
|
|
│ │ ├── base_layer.js # Abstract base class
|
|
│ │ ├── points_layer.js # Point markers
|
|
│ │ ├── routes_layer.js # Route lines
|
|
│ │ ├── heatmap_layer.js # Heatmap visualization
|
|
│ │ ├── visits_layer.js # Visit markers
|
|
│ │ ├── photos_layer.js # Photo markers
|
|
│ │ ├── places_layer.js # Places markers
|
|
│ │ ├── areas_layer.js # User-defined areas
|
|
│ │ ├── fog_layer.js # Fog of war overlay
|
|
│ │ └── scratch_layer.js # Scratch map
|
|
│ ├── services/ # API and external services
|
|
│ │ ├── api_client.js # REST API wrapper
|
|
│ │ └── location_search_service.js
|
|
│ ├── utils/ # Helper utilities
|
|
│ │ ├── settings_manager.js # User preferences
|
|
│ │ ├── geojson_transformers.js
|
|
│ │ ├── performance_monitor.js
|
|
│ │ ├── lazy_loader.js # Code splitting
|
|
│ │ └── ...
|
|
│ ├── components/ # Reusable UI components
|
|
│ │ ├── popup_factory.js # Map popup generator
|
|
│ │ ├── toast.js # Toast notifications
|
|
│ │ └── ...
|
|
│ └── channels/ # ActionCable channels
|
|
│ └── map_channel.js # Real-time updates
|
|
└── maps/ # Legacy Maps V1 (being phased out)
|
|
```
|
|
|
|
## Core Concepts
|
|
|
|
### Manager Pattern
|
|
|
|
The Maps (MapLibre) controller delegates responsibilities to specialized managers:
|
|
|
|
1. **LayerManager** - Layer lifecycle (add/remove/toggle/update)
|
|
2. **DataLoader** - API data fetching and transformation
|
|
3. **EventHandlers** - Map interaction events
|
|
4. **FilterManager** - Data filtering and searching
|
|
5. **DateManager** - Date range calculations
|
|
6. **SettingsManager** - User preferences persistence
|
|
|
|
**Benefits:**
|
|
- Single Responsibility Principle
|
|
- Easier testing
|
|
- Improved code organization
|
|
- Better reusability
|
|
|
|
### Data Flow
|
|
|
|
```
|
|
User Action
|
|
↓
|
|
Stimulus Controller Method
|
|
↓
|
|
Manager (e.g., DataLoader)
|
|
↓
|
|
Service (e.g., ApiClient)
|
|
↓
|
|
API Endpoint
|
|
↓
|
|
Transform to GeoJSON
|
|
↓
|
|
Update Layer
|
|
↓
|
|
MapLibre Renders
|
|
```
|
|
|
|
### State Management
|
|
|
|
**Settings Persistence:**
|
|
- Primary: Backend API (`/api/v1/settings`)
|
|
- Fallback: localStorage
|
|
- Sync on initialization
|
|
- Save on every change (debounced)
|
|
|
|
**Layer State:**
|
|
- Stored in layer instances (`this.visible`, `this.data`)
|
|
- Synced with SettingsManager
|
|
- Persisted across sessions
|
|
|
|
### Event System
|
|
|
|
**Custom Events:**
|
|
```javascript
|
|
// Dispatch
|
|
document.dispatchEvent(new CustomEvent('visit:created', {
|
|
detail: { visitId: 123 }
|
|
}))
|
|
|
|
// Listen
|
|
document.addEventListener('visit:created', (event) => {
|
|
console.log(event.detail.visitId)
|
|
})
|
|
```
|
|
|
|
**Map Events:**
|
|
```javascript
|
|
map.on('click', 'layer-id', (e) => {
|
|
const feature = e.features[0]
|
|
// Handle click
|
|
})
|
|
```
|
|
|
|
## Maps (MapLibre) Architecture
|
|
|
|
### Layer Hierarchy
|
|
|
|
Layers are rendered in specific order (bottom to top):
|
|
|
|
1. **Scratch Layer** - Visited countries/regions overlay
|
|
2. **Heatmap Layer** - Point density visualization
|
|
3. **Areas Layer** - User-defined circular areas
|
|
4. **Tracks Layer** - Imported GPS tracks
|
|
5. **Routes Layer** - Generated routes from points
|
|
6. **Visits Layer** - Detected visits to places
|
|
7. **Places Layer** - Named locations
|
|
8. **Photos Layer** - Photos with geolocation
|
|
9. **Family Layer** - Real-time family member locations
|
|
10. **Points Layer** - Individual location points
|
|
11. **Fog Layer** - Canvas overlay showing unexplored areas
|
|
|
|
### BaseLayer Pattern
|
|
|
|
All layers extend `BaseLayer` which provides:
|
|
|
|
**Methods:**
|
|
- `add(data)` - Add layer to map
|
|
- `update(data)` - Update layer data
|
|
- `remove()` - Remove layer from map
|
|
- `show()` / `hide()` - Toggle visibility
|
|
- `toggle(visible)` - Set visibility state
|
|
|
|
**Abstract Methods (must implement):**
|
|
- `getSourceConfig()` - MapLibre source configuration
|
|
- `getLayerConfigs()` - Array of MapLibre layer configurations
|
|
|
|
**Example Implementation:**
|
|
```javascript
|
|
export class PointsLayer extends BaseLayer {
|
|
constructor(map, options = {}) {
|
|
super(map, { id: 'points', ...options })
|
|
}
|
|
|
|
getSourceConfig() {
|
|
return {
|
|
type: 'geojson',
|
|
data: this.data || { type: 'FeatureCollection', features: [] }
|
|
}
|
|
}
|
|
|
|
getLayerConfigs() {
|
|
return [{
|
|
id: 'points',
|
|
type: 'circle',
|
|
source: this.sourceId,
|
|
paint: {
|
|
'circle-radius': 4,
|
|
'circle-color': '#3b82f6'
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Lazy Loading
|
|
|
|
Heavy layers are lazy-loaded to reduce initial bundle size:
|
|
|
|
```javascript
|
|
// In lazy_loader.js
|
|
const paths = {
|
|
'fog': () => import('../layers/fog_layer.js'),
|
|
'scratch': () => import('../layers/scratch_layer.js')
|
|
}
|
|
|
|
// Usage
|
|
const ScratchLayer = await lazyLoader.loadLayer('scratch')
|
|
const layer = new ScratchLayer(map, options)
|
|
```
|
|
|
|
**When to use:**
|
|
- Large dependencies (e.g., canvas-based rendering)
|
|
- Rarely-used features
|
|
- Heavy computations
|
|
|
|
### GeoJSON Transformations
|
|
|
|
All data is transformed to GeoJSON before rendering:
|
|
|
|
```javascript
|
|
// Points
|
|
{
|
|
type: 'FeatureCollection',
|
|
features: [{
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [longitude, latitude]
|
|
},
|
|
properties: {
|
|
id: 1,
|
|
timestamp: '2024-01-01T12:00:00Z',
|
|
// ... other properties
|
|
}
|
|
}]
|
|
}
|
|
```
|
|
|
|
**Key Functions:**
|
|
- `pointsToGeoJSON(points)` - Convert points array
|
|
- `visitsToGeoJSON(visits)` - Convert visits
|
|
- `photosToGeoJSON(photos)` - Convert photos
|
|
- `placesToGeoJSON(places)` - Convert places
|
|
- `areasToGeoJSON(areas)` - Convert circular areas to polygons
|
|
|
|
## Creating New Features
|
|
|
|
### Adding a New Layer
|
|
|
|
1. **Create layer class** in `app/javascript/maps_maplibre/layers/`:
|
|
|
|
```javascript
|
|
import { BaseLayer } from './base_layer'
|
|
|
|
export class NewLayer extends BaseLayer {
|
|
constructor(map, options = {}) {
|
|
super(map, { id: 'new-layer', ...options })
|
|
}
|
|
|
|
getSourceConfig() {
|
|
return {
|
|
type: 'geojson',
|
|
data: this.data || { type: 'FeatureCollection', features: [] }
|
|
}
|
|
}
|
|
|
|
getLayerConfigs() {
|
|
return [{
|
|
id: this.id,
|
|
type: 'symbol', // or 'circle', 'line', 'fill', 'heatmap'
|
|
source: this.sourceId,
|
|
paint: { /* styling */ },
|
|
layout: { /* layout */ }
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Register in LayerManager** (`controllers/maps_maplibre/layer_manager.js`):
|
|
|
|
```javascript
|
|
import { NewLayer } from 'maps_maplibre/layers/new_layer'
|
|
|
|
// In addAllLayers method
|
|
_addNewLayer(dataGeoJSON) {
|
|
if (!this.layers.newLayer) {
|
|
this.layers.newLayer = new NewLayer(this.map, {
|
|
visible: this.settings.newLayerEnabled || false
|
|
})
|
|
this.layers.newLayer.add(dataGeoJSON)
|
|
} else {
|
|
this.layers.newLayer.update(dataGeoJSON)
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **Add to settings** (`utils/settings_manager.js`):
|
|
|
|
```javascript
|
|
const DEFAULT_SETTINGS = {
|
|
// ...
|
|
newLayerEnabled: false
|
|
}
|
|
|
|
const LAYER_NAME_MAP = {
|
|
// ...
|
|
'New Layer': 'newLayerEnabled'
|
|
}
|
|
```
|
|
|
|
4. **Add UI controls** in view template.
|
|
|
|
### Adding a New API Endpoint
|
|
|
|
1. **Add method to ApiClient** (`services/api_client.js`):
|
|
|
|
```javascript
|
|
async fetchNewData({ param1, param2 }) {
|
|
const params = new URLSearchParams({ param1, param2 })
|
|
|
|
const response = await fetch(`${this.baseURL}/new-endpoint?${params}`, {
|
|
headers: this.getHeaders()
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch: ${response.statusText}`)
|
|
}
|
|
|
|
return response.json()
|
|
}
|
|
```
|
|
|
|
2. **Add transformation** in DataLoader:
|
|
|
|
```javascript
|
|
newDataToGeoJSON(data) {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: data.map(item => ({
|
|
type: 'Feature',
|
|
geometry: { /* ... */ },
|
|
properties: { /* ... */ }
|
|
}))
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **Use in controller:**
|
|
|
|
```javascript
|
|
const data = await this.api.fetchNewData({ param1, param2 })
|
|
const geojson = this.dataLoader.newDataToGeoJSON(data)
|
|
this.layerManager.updateLayer('new-layer', geojson)
|
|
```
|
|
|
|
### Adding a New Utility
|
|
|
|
1. **Create utility file** in `utils/`:
|
|
|
|
```javascript
|
|
export class NewUtility {
|
|
static calculate(input) {
|
|
// Pure function - no side effects
|
|
return result
|
|
}
|
|
}
|
|
|
|
// Or singleton for stateful utilities
|
|
class NewManager {
|
|
constructor() {
|
|
this.state = {}
|
|
}
|
|
|
|
doSomething() {
|
|
// Stateful operation
|
|
}
|
|
}
|
|
|
|
export const newManager = new NewManager()
|
|
```
|
|
|
|
2. **Import and use:**
|
|
|
|
```javascript
|
|
import { NewUtility } from 'maps_maplibre/utils/new_utility'
|
|
|
|
const result = NewUtility.calculate(input)
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Code Style
|
|
|
|
1. **Use ES6+ features:**
|
|
- Arrow functions
|
|
- Template literals
|
|
- Destructuring
|
|
- Async/await
|
|
- Classes
|
|
|
|
2. **Naming conventions:**
|
|
- Classes: `PascalCase`
|
|
- Methods/variables: `camelCase`
|
|
- Constants: `UPPER_SNAKE_CASE`
|
|
- Files: `snake_case.js`
|
|
|
|
3. **Always use semicolons** for statement termination
|
|
|
|
4. **Prefer `const` over `let`**, avoid `var`
|
|
|
|
### Performance
|
|
|
|
1. **Lazy load heavy features:**
|
|
```javascript
|
|
const Layer = await lazyLoader.loadLayer('name')
|
|
```
|
|
|
|
2. **Debounce frequent operations:**
|
|
```javascript
|
|
let timeout
|
|
function onInput(e) {
|
|
clearTimeout(timeout)
|
|
timeout = setTimeout(() => actualWork(e), 300)
|
|
}
|
|
```
|
|
|
|
3. **Use performance monitoring:**
|
|
```javascript
|
|
performanceMonitor.mark('operation')
|
|
// ... do work
|
|
performanceMonitor.measure('operation')
|
|
```
|
|
|
|
4. **Minimize DOM manipulations** - batch updates when possible
|
|
|
|
### Error Handling
|
|
|
|
1. **Always handle promise rejections:**
|
|
```javascript
|
|
try {
|
|
const data = await fetchData()
|
|
} catch (error) {
|
|
console.error('Failed:', error)
|
|
Toast.error('Operation failed')
|
|
}
|
|
```
|
|
|
|
2. **Provide user feedback:**
|
|
```javascript
|
|
Toast.success('Data loaded')
|
|
Toast.error('Failed to load data')
|
|
Toast.info('Click map to add point')
|
|
```
|
|
|
|
3. **Log errors for debugging:**
|
|
```javascript
|
|
console.error('[Component] Error details:', error)
|
|
```
|
|
|
|
### Memory Management
|
|
|
|
1. **Always cleanup in disconnect():**
|
|
```javascript
|
|
disconnect() {
|
|
this.searchManager?.destroy()
|
|
this.cleanup.cleanup()
|
|
this.map?.remove()
|
|
}
|
|
```
|
|
|
|
2. **Use CleanupHelper for event listeners:**
|
|
```javascript
|
|
this.cleanup = new CleanupHelper()
|
|
this.cleanup.addEventListener(element, 'click', handler)
|
|
|
|
// In disconnect():
|
|
this.cleanup.cleanup() // Removes all listeners
|
|
```
|
|
|
|
3. **Remove map layers and sources:**
|
|
```javascript
|
|
remove() {
|
|
this.getLayerIds().forEach(id => {
|
|
if (this.map.getLayer(id)) {
|
|
this.map.removeLayer(id)
|
|
}
|
|
})
|
|
if (this.map.getSource(this.sourceId)) {
|
|
this.map.removeSource(this.sourceId)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Considerations
|
|
|
|
1. **Keep methods small and focused** - easier to test
|
|
2. **Avoid tight coupling** - use dependency injection
|
|
3. **Separate pure functions** from side effects
|
|
4. **Use static methods** for stateless utilities
|
|
|
|
### State Management
|
|
|
|
1. **Single source of truth:**
|
|
- Settings: `SettingsManager`
|
|
- Layer data: Layer instances
|
|
- UI state: Controller properties
|
|
|
|
2. **Sync state with backend:**
|
|
```javascript
|
|
SettingsManager.updateSetting('key', value)
|
|
// Saves to both localStorage and backend
|
|
```
|
|
|
|
3. **Restore state on load:**
|
|
```javascript
|
|
async connect() {
|
|
this.settings = await SettingsManager.sync()
|
|
this.syncToggleStates()
|
|
}
|
|
```
|
|
|
|
### Documentation
|
|
|
|
1. **Add JSDoc comments for public APIs:**
|
|
```javascript
|
|
/**
|
|
* Fetch all points for date range
|
|
* @param {Object} options - { start_at, end_at, onProgress }
|
|
* @returns {Promise<Array>} All points
|
|
*/
|
|
async fetchAllPoints({ start_at, end_at, onProgress }) {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
2. **Document complex logic with inline comments**
|
|
|
|
3. **Keep this README updated** when adding major features
|
|
|
|
### Code Organization
|
|
|
|
1. **One class per file** - easier to find and maintain
|
|
2. **Group related functionality** in directories
|
|
3. **Use index files** for barrel exports when needed
|
|
4. **Avoid circular dependencies** - use dependency injection
|
|
|
|
### Migration from Maps V1 to V2
|
|
|
|
When updating features, follow this pattern:
|
|
|
|
1. **Keep V1 working** - V2 is opt-in
|
|
2. **Share utilities** where possible (e.g., color calculations)
|
|
3. **Use same API endpoints** - maintain compatibility
|
|
4. **Document differences** in code comments
|
|
|
|
---
|
|
|
|
## Examples
|
|
|
|
### Complete Layer Implementation
|
|
|
|
See `app/javascript/maps_maplibre/layers/heatmap_layer.js` for a simple example.
|
|
|
|
### Complete Utility Implementation
|
|
|
|
See `app/javascript/maps_maplibre/utils/settings_manager.js` for state management.
|
|
|
|
### Complete Service Implementation
|
|
|
|
See `app/javascript/maps_maplibre/services/api_client.js` for API communication.
|
|
|
|
### Complete Controller Implementation
|
|
|
|
See `app/javascript/controllers/maps/maplibre_controller.js` for orchestration.
|
|
|
|
---
|
|
|
|
**Questions or need help?** Check the existing code for patterns or ask in Discord: https://discord.gg/pHsBjpt5J8
|