Fantasy-Map-Generator/api-server/client.js
Claude 20458e39e2
Add external API integration for wiki/web UI synchronization
This commit adds a comprehensive external API system that allows external tools
(wikis, web UIs, etc.) to control and synchronize with Fantasy Map Generator.

New Features:
- External API Bridge (modules/external-api.js)
  - Event-driven architecture with EventEmitter
  - Map lifecycle control (create, load, save)
  - Data access methods (rivers, cultures, states, burgs)
  - Data mutation methods with auto-redraw
  - Export support (SVG, PNG, JSON)
  - Change detection with automatic event emission

- PostMessage Communication Layer
  - Auto-enables when FMG is embedded in iframe
  - Bidirectional message passing
  - Request/response pattern with promise support
  - Automatic event forwarding to parent window

- REST API Server (api-server/)
  - Express.js server with full CRUD operations
  - WebSocket support via Socket.IO for real-time updates
  - File upload support for map and CSV import
  - In-memory storage (can be replaced with database)
  - CORS enabled for cross-origin requests
  - Comprehensive endpoints for all map data

- Client Library (api-server/client.js)
  - Simple JavaScript client for REST API
  - Promise-based async methods
  - Works in browser and Node.js

- Demo Pages (demos/)
  - PostMessage integration demo with full UI
  - REST API demo with interactive testing
  - WebSocket demo for real-time events

- Documentation
  - Comprehensive integration guide (EXTERNAL_API_INTEGRATION.md)
  - API reference with TypeScript interfaces
  - Multiple integration examples
  - Troubleshooting guide

Integration Methods:
1. PostMessage Bridge - For iframe embedding
2. REST API - For server-side integration
3. Direct JavaScript API - For same-origin apps

Use Cases:
- Wiki pages that need to display and control maps
- Web UIs that want to edit map data
- External tools that need to sync with FMG
- Real-time collaborative map editing
- Batch operations and automation

Technical Details:
- Zero dependencies for external-api.js (pure JS)
- Auto-initializes on DOMContentLoaded
- Throttled change detection (500ms debounce)
- Deep cloning for data access (prevents mutations)
- Error handling throughout
- Version tagged (v1.0.0)

Updated Files:
- index.html: Added script tag to load external-api module

All APIs are backward compatible and don't affect existing functionality.
2025-11-04 21:43:06 +00:00

260 lines
6.4 KiB
JavaScript

/**
* Fantasy Map Generator - JavaScript Client Library
*
* A simple client library for interacting with the FMG REST API
*
* Usage:
* const client = new FMGClient('http://localhost:3000/api');
* const map = await client.createMap({ seed: 'my-world' });
*/
class FMGClient {
constructor(baseUrl = 'http://localhost:3000/api') {
this.baseUrl = baseUrl;
}
/**
* Make API request
* @private
*/
async _request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
method: options.method || 'GET',
headers: {
...options.headers
}
};
if (options.body) {
if (options.body instanceof FormData) {
config.body = options.body;
} else {
config.headers['Content-Type'] = 'application/json';
config.body = JSON.stringify(options.body);
}
}
try {
const response = await fetch(url, config);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`);
}
return data;
} catch (error) {
throw new Error(`API request failed: ${error.message}`);
}
}
// ============================================================================
// HEALTH
// ============================================================================
/**
* Check API health
* @returns {Promise<Object>} Health status
*/
async health() {
return this._request('/health');
}
// ============================================================================
// MAP OPERATIONS
// ============================================================================
/**
* Create a new map
* @param {Object} options - Map options
* @param {string} options.seed - Map seed
* @param {number} options.width - Map width
* @param {number} options.height - Map height
* @returns {Promise<Object>} Created map info
*/
async createMap(options = {}) {
return this._request('/maps', {
method: 'POST',
body: options
});
}
/**
* Get map by ID
* @param {string} mapId - Map ID
* @returns {Promise<Object>} Map data
*/
async getMap(mapId) {
return this._request(`/maps/${mapId}`);
}
/**
* List all maps
* @returns {Promise<Object>} Map list
*/
async listMaps() {
return this._request('/maps');
}
/**
* Update map
* @param {string} mapId - Map ID
* @param {Object} data - Map data
* @returns {Promise<Object>} Updated map
*/
async updateMap(mapId, data) {
return this._request(`/maps/${mapId}`, {
method: 'PUT',
body: { data }
});
}
/**
* Delete map
* @param {string} mapId - Map ID
* @returns {Promise<Object>} Deletion result
*/
async deleteMap(mapId) {
return this._request(`/maps/${mapId}`, {
method: 'DELETE'
});
}
/**
* Load map from file
* @param {string} mapId - Map ID
* @param {File} file - Map file
* @returns {Promise<Object>} Load result
*/
async loadMap(mapId, file) {
const formData = new FormData();
formData.append('file', file);
return this._request(`/maps/${mapId}/load`, {
method: 'POST',
body: formData
});
}
// ============================================================================
// RIVERS
// ============================================================================
/**
* Get rivers
* @param {string} mapId - Map ID
* @returns {Promise<Array>} Rivers array
*/
async getRivers(mapId) {
const result = await this._request(`/maps/${mapId}/rivers`);
return result.rivers;
}
/**
* Update rivers
* @param {string} mapId - Map ID
* @param {Array} rivers - Rivers array
* @returns {Promise<Object>} Update result
*/
async updateRivers(mapId, rivers) {
return this._request(`/maps/${mapId}/rivers`, {
method: 'PUT',
body: { rivers }
});
}
/**
* Import rivers from CSV
* @param {string} mapId - Map ID
* @param {File} csvFile - CSV file
* @returns {Promise<Object>} Import result
*/
async importRiversCSV(mapId, csvFile) {
const formData = new FormData();
formData.append('file', csvFile);
return this._request(`/maps/${mapId}/rivers/import`, {
method: 'POST',
body: formData
});
}
// ============================================================================
// CULTURES
// ============================================================================
/**
* Get cultures
* @param {string} mapId - Map ID
* @returns {Promise<Array>} Cultures array
*/
async getCultures(mapId) {
const result = await this._request(`/maps/${mapId}/cultures`);
return result.cultures;
}
// ============================================================================
// STATES
// ============================================================================
/**
* Get states
* @param {string} mapId - Map ID
* @returns {Promise<Array>} States array
*/
async getStates(mapId) {
const result = await this._request(`/maps/${mapId}/states`);
return result.states;
}
// ============================================================================
// BURGS
// ============================================================================
/**
* Get burgs (cities/towns)
* @param {string} mapId - Map ID
* @returns {Promise<Array>} Burgs array
*/
async getBurgs(mapId) {
const result = await this._request(`/maps/${mapId}/burgs`);
return result.burgs;
}
/**
* Add new burg
* @param {string} mapId - Map ID
* @param {Object} burgData - Burg data
* @returns {Promise<Object>} Created burg
*/
async addBurg(mapId, burgData) {
return this._request(`/maps/${mapId}/burgs`, {
method: 'POST',
body: burgData
});
}
// ============================================================================
// EXPORT
// ============================================================================
/**
* Export map in specified format
* @param {string} mapId - Map ID
* @param {string} format - Export format (svg, png, json, data)
* @returns {Promise<Object>} Export result
*/
async exportMap(mapId, format) {
return this._request(`/maps/${mapId}/export/${format}`);
}
}
// Export for Node.js and browser
if (typeof module !== 'undefined' && module.exports) {
module.exports = FMGClient;
}
if (typeof window !== 'undefined') {
window.FMGClient = FMGClient;
}