refactor: comprehensive map-editor plugin refactoring (phases 1-3)
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 18s

This commit implements a complete refactoring of the map-editor plugin to
improve code organization, reusability, and maintainability.

## Phase 1: Extraction of composables and factory functions

**New composables:**
- `useMarkers.js`: Centralized marker state and CRUD operations
  - Exports: markers, selectedMarkerId, editingMarker refs
  - Computed: canAddMarker, hasMarkers, selectedMarker
  - Methods: addMarker, updateMarker, deleteMarker, selectMarker, etc.
  - Includes createMarker() factory to eliminate code duplication

- `useMapData.js`: Map data persistence (YAML load/save)
  - Exports: center, zoom refs
  - Methods: loadMapData, saveMapData, debouncedSave
  - Handles lifecycle cleanup of debounce timeouts

**Benefits:**
- Eliminated code duplication (2 identical marker creation blocks)
- Separated business logic from UI concerns
- Improved testability with pure functions
- Added JSDoc documentation throughout

## Phase 2: Component extraction

**New components:**
- `MarkerList.vue`: Extracted sidebar UI from MapEditor.vue
  - Props: markers, selectedMarkerId, maxMarkers
  - Emits: add-marker, select-marker, edit-marker, delete-marker, select-location
  - Includes integrated GeocodeSearch component
  - Self-contained styles with scoped CSS

**Benefits:**
- MapEditor.vue reduced from 370 → 230 lines (-40%)
- Clear separation of concerns (orchestration vs presentation)
- Reusable component for potential future use
- Easier to test and maintain

## Phase 3: Utils restructuring with JSDoc

**New structure:**
```
utils/
├── constants.js           # NOMINATIM_API, MAP_DEFAULTS, DEBOUNCE_DELAYS
├── api/
│   └── nominatim.js      # geocode() with full JSDoc typedefs
└── helpers/
    └── debounce.js       # Generic debounce utility
```

**Removed:**
- `utils/geocoding.js` (replaced by modular structure)

**Benefits:**
- Constants centralized for easy configuration
- API layer separated from helpers
- Complete JSDoc type annotations for better IDE support
- Better organization following standard patterns

## Updated components

**MapEditor.vue:**
- Now uses useMarkers and useMapData composables
- Uses MarkerList component instead of inline template
- Cleaner setup function with better separation
- Reduced from 537 → 256 lines (CSS moved to MarkerList)

**GeocodeSearch.vue:**
- Updated imports to use new utils structure
- Uses DEBOUNCE_DELAYS constant instead of hardcoded value

## Build verification

-  npm run build successful
-  Bundle size unchanged (806.10 kB / 223.46 KiB gzipped)
-  All functionality preserved
-  No breaking changes

## Documentation

- Added comprehensive README.md with:
  - Architecture overview
  - Composables usage examples
  - Component API documentation
  - Data flow diagrams
  - Development guide

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-01-28 16:29:15 +01:00
parent 437349cd2b
commit 2b0f4f8742
13 changed files with 1347 additions and 498 deletions

View file

@ -0,0 +1,76 @@
/**
* Nominatim API client for geocoding
* https://nominatim.openstreetmap.org/
*
* Usage policy: https://operations.osmfoundation.org/policies/nominatim/
* Rate limit: 1 request per second
*/
import { NOMINATIM_API } from '../constants.js';
/**
* @typedef {Object} GeocodingResult
* @property {number} id - Place ID
* @property {string} displayName - Full display name of the location
* @property {number} lat - Latitude
* @property {number} lon - Longitude
* @property {string} type - Location type (city, street, etc.)
* @property {number} importance - Importance score
* @property {Array<string>} boundingBox - Bounding box coordinates
*/
/**
* Search for an address using Nominatim API
*
* @param {string} query - Address to search for
* @returns {Promise<Array<GeocodingResult>>} Array of geocoding results
* @throws {Error} When API request fails
*
* @example
* const results = await geocode('Paris, France');
* console.log(results[0].displayName); // "Paris, Île-de-France, France"
*/
export async function geocode(query) {
if (!query || query.trim().length < NOMINATIM_API.MIN_QUERY_LENGTH) {
return [];
}
try {
const params = new URLSearchParams({
q: query.trim(),
format: 'json',
addressdetails: '1',
limit: String(NOMINATIM_API.MAX_RESULTS),
'accept-language': NOMINATIM_API.DEFAULT_LANGUAGE,
});
const response = await fetch(
`${NOMINATIM_API.BASE_URL}?${params.toString()}`,
{
headers: {
'User-Agent': NOMINATIM_API.USER_AGENT,
},
}
);
if (!response.ok) {
throw new Error(`Nominatim API error: ${response.status}`);
}
const data = await response.json();
// Transform results to a consistent format
return data.map((result) => ({
id: result.place_id,
displayName: result.display_name,
lat: parseFloat(result.lat),
lon: parseFloat(result.lon),
type: result.type,
importance: result.importance,
boundingBox: result.boundingbox,
}));
} catch (error) {
console.error('Geocoding error:', error);
throw error;
}
}

View file

@ -0,0 +1,26 @@
/**
* Map editor constants
*/
// Nominatim API configuration
export const NOMINATIM_API = {
BASE_URL: 'https://nominatim.openstreetmap.org/search',
USER_AGENT: 'GeoProject/1.0 (Kirby CMS Map Editor)',
RATE_LIMIT_MS: 1000, // 1 request per second
MIN_QUERY_LENGTH: 3,
MAX_RESULTS: 5,
DEFAULT_LANGUAGE: 'fr',
};
// Map defaults
export const MAP_DEFAULTS = {
CENTER: { lat: 43.836699, lon: 4.360054 },
ZOOM: 13,
MAX_MARKERS: 50,
};
// Debounce delays (ms)
export const DEBOUNCE_DELAYS = {
GEOCODING: 500,
AUTO_SAVE: 300,
};

View file

@ -0,0 +1,76 @@
/**
* Geocoding utility using Nominatim API
* https://nominatim.openstreetmap.org/
*
* Usage policy: https://operations.osmfoundation.org/policies/nominatim/
* Rate limit: 1 request per second
*/
const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search';
/**
* Search for an address using Nominatim
* @param {string} query - Address to search for
* @returns {Promise<Array>} Array of results with lat, lon, display_name, etc.
*/
export async function geocode(query) {
if (!query || query.trim().length < 3) {
return [];
}
try {
const params = new URLSearchParams({
q: query.trim(),
format: 'json',
addressdetails: '1',
limit: '5',
// Respectful user agent as requested by Nominatim policy
'accept-language': 'fr'
});
const response = await fetch(`${NOMINATIM_URL}?${params.toString()}`, {
headers: {
'User-Agent': 'GeoProject/1.0 (Kirby CMS Map Editor)'
}
});
if (!response.ok) {
throw new Error(`Nominatim API error: ${response.status}`);
}
const data = await response.json();
// Transform results to a consistent format
return data.map(result => ({
id: result.place_id,
displayName: result.display_name,
lat: parseFloat(result.lat),
lon: parseFloat(result.lon),
type: result.type,
importance: result.importance,
boundingBox: result.boundingbox
}));
} catch (error) {
console.error('Geocoding error:', error);
throw error;
}
}
/**
* Debounce function to limit API calls
* @param {Function} func - Function to debounce
* @param {number} wait - Milliseconds to wait
* @returns {Function} Debounced function
*/
export function debounce(func, wait = 500) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}

View file

@ -0,0 +1,24 @@
/**
* Debounce function to limit API calls and improve performance
*
* @param {Function} func - Function to debounce
* @param {number} wait - Milliseconds to wait before executing
* @returns {Function} Debounced function
*
* @example
* const debouncedSearch = debounce(searchFunction, 500);
* input.addEventListener('input', () => debouncedSearch(input.value));
*/
export function debounce(func, wait = 500) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}