diff --git a/App.tsx b/App.tsx index 87e7673..0d0f9d8 100644 --- a/App.tsx +++ b/App.tsx @@ -14,6 +14,10 @@ import DatapackGenerator from './pages/DatapackGenerator'; import DatabaseManager from './pages/DatabaseManager'; import LinkPlayer from './pages/LinkPlayer'; import AdminPage from './pages/Admin'; +import AdminMapManagement from './pages/AdminMapManagement'; +import WorldMap from './pages/WorldMap'; +import EditMarker from './pages/EditMarker'; +import DocumentationPage from './pages/Dokumentation'; import { dbService } from './services/DatabaseService'; import { authService } from './services/AuthService'; import { DiscordUser } from './types'; @@ -131,6 +135,14 @@ function App() { {/* Admin Routes */} navigate('/')} />} /> + } /> + + {/* Map Route */} + } /> + } /> + + {/* Dokumentation Route */} + } /> {/* Fallback */} /world/XaeroWaypoints/dim0/ +``` + +Look for files named like: +- `xaero_map_0_0.png` +- `xaero_map_1_0.png` +- `xaero_map_0_1.png` +- etc. + +## Step 3: Upload Files to Backend Container + +The map tiles should be uploaded to the backend container's `uploads/map/` directory: + +1. **For Docker Setup**: Upload PNG files to the `uploads/map/` directory inside the backend container +2. **For Direct Upload**: Use the file upload functionality in your admin panel to upload PNG files to `uploads/map/` +3. **File Naming**: Ensure files follow the pattern `xaero_map_x_y.png` (e.g., `xaero_map_0_0.png`, `xaero_map_1_0.png`) + +**Note**: The map assembly process automatically looks for tiles in the backend's `uploads/map/` directory. + +## Step 4: Run Map Assembly + +1. Go to `/admin/map-management` in your admin panel +2. Click "Karten-Zusammenstellung starten" +3. The system will automatically process all PNG files and create the web map + +## Troubleshooting + +### No Tiles Found +- Ensure PNG files are in the correct directory +- Check that files follow the naming pattern `xaero_map_x_y.png` +- Verify file permissions allow the server to read the files + +### Assembly Fails +- Check server logs for error messages +- Ensure sufficient disk space for processing +- Verify PNG files are not corrupted + +### Map Not Displaying +- Check that the assembly completed successfully +- Verify the `map-metadata.json` file was created +- Ensure the frontend can access the generated map tiles + +## Example Directory Structure + +``` +projekt_vollidion_website/ +├── map-tiles/ +│ ├── xaero_map_0_0.png +│ ├── xaero_map_1_0.png +│ ├── xaero_map_0_1.png +│ ├── xaero_map_1_1.png +│ └── ... +├── backend/ +├── frontend/ +└── ... +``` + +## Performance Notes + +- Large maps (100+ tiles) may take several minutes to process +- The assembly process is memory-intensive for very large maps +- Consider using a powerful server for initial map generation + +## Support + +If you encounter issues: +1. Check the browser console for JavaScript errors +2. Review the server logs for backend errors +3. Verify all PNG files are valid and accessible +4. Ensure the map-tiles directory exists and is writable diff --git a/backend/database.js b/backend/database.js index c5c20dd..cfd7ddc 100644 --- a/backend/database.js +++ b/backend/database.js @@ -2,9 +2,9 @@ const mysql = require('mysql2'); // Database Config from Env const dbConfig = { - host: process.env.DB_HOST || 'localhost', - user: process.env.DB_USER || 'root', - password: process.env.DB_PASS || '', + host: process.env.DB_HOST || '192.168.1.102', + user: process.env.DB_USER || 'obsidian_user', + password: process.env.DB_PASS || 'obsidian_pass', database: process.env.DB_NAME || 'obsidian_db', waitForConnections: true, connectionLimit: 10, @@ -90,6 +90,41 @@ const SEED_PROJECTS = [ } ]; +const SEED_MAP_LAYERS = [ + { id: 'layer-1', name: 'Städte', description: 'Alle Städte und Siedlungen', order_index: 1 }, + { id: 'layer-2', name: 'Points of Interest', description: 'Besondere Orte und Sehenswürdigkeiten', order_index: 2 }, + { id: 'layer-3', name: 'Spieler-Häuser', description: 'Spieler-Wohnsitze und Häuser', order_index: 3 } +]; + +const SEED_MAP_MARKERS = [ + { + id: 'marker-1', + name: 'Provisorium Null', + type: 'city', + x_coord: -2560, + z_coord: 512, + description: 'Die erste Siedlung der neuen Ära', + linked_entity_type: 'organization', + linked_entity_id: 'org-3', + icon_type: 'city', + color: '#2563eb', + is_public: 1 + }, + { + id: 'marker-2', + name: 'Sakura', + type: 'city', + x_coord: 1536, + z_coord: -512, + description: 'Eine dunkle, biolumineszente Hafenstadt', + linked_entity_type: 'organization', + linked_entity_id: 'org-4', + icon_type: 'city', + color: '#dc2626', + is_public: 1 + } +]; + // Retry connection logic for Docker function init() { const tryConnect = () => { @@ -169,6 +204,34 @@ function setupTables() { logoImageId VARCHAR(50), shopCatalog JSON, gallery JSON + )`, + `CREATE TABLE IF NOT EXISTS map_markers ( + id VARCHAR(50) PRIMARY KEY, + name VARCHAR(255), + type VARCHAR(50), -- 'city', 'poi', 'player_home', 'waypoint' + x_coord INTEGER, + z_coord INTEGER, + description TEXT, + linked_entity_type VARCHAR(50), -- 'city', 'organization', 'player' + linked_entity_id VARCHAR(50), + icon_type VARCHAR(50), -- 'city', 'house', 'chest', 'flag', etc. + color VARCHAR(7), -- hex color code + is_public TINYINT DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + )`, + `CREATE TABLE IF NOT EXISTS map_layers ( + id VARCHAR(50) PRIMARY KEY, + name VARCHAR(255), + description TEXT, + is_active TINYINT DEFAULT 1, + order_index INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + `CREATE TABLE IF NOT EXISTS map_metadata ( + key VARCHAR(100) PRIMARY KEY, + value TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP )` ]; @@ -212,6 +275,28 @@ function seedData() { }); } }); + + // Seed map layers + pool.query("SELECT COUNT(*) as count FROM map_layers", (err, rows) => { + if (!err && rows[0].count === 0) { + console.log("Seeding Map Layers..."); + SEED_MAP_LAYERS.forEach(layer => { + pool.query("INSERT INTO map_layers VALUES (?, ?, ?, ?, ?, ?)", + [layer.id, layer.name, layer.description, 1, layer.order_index, new Date().toISOString().slice(0, 19).replace('T', ' ')]); + }); + } + }); + + // Seed map markers + pool.query("SELECT COUNT(*) as count FROM map_markers", (err, rows) => { + if (!err && rows[0].count === 0) { + console.log("Seeding Map Markers..."); + SEED_MAP_MARKERS.forEach(marker => { + pool.query("INSERT INTO map_markers (id, name, type, x_coord, z_coord, description, linked_entity_type, linked_entity_id, icon_type, color, is_public) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [marker.id, marker.name, marker.type, marker.x_coord, marker.z_coord, marker.description, marker.linked_entity_type, marker.linked_entity_id, marker.icon_type, marker.color, marker.is_public]); + }); + } + }); } // Wrapper to mimic SQLite API for easy migration in server.js diff --git a/backend/debug-map-detailed.js b/backend/debug-map-detailed.js new file mode 100644 index 0000000..2bfe03a --- /dev/null +++ b/backend/debug-map-detailed.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +// Detailed debug script for map processing +const debugInterface = require('./server.js'); +const { db } = require('./database'); + +console.log('🔧 Detailed Map Processor Debug Console'); +console.log('====================================='); + +async function runDetailedDebug() { + try { + console.log('1. Testing database connection...'); + const dbTest = await new Promise((resolve, reject) => { + db.get("SELECT 1 as test", [], (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); + console.log('✅ Database connection successful:', dbTest); + + console.log('\n2. Testing tile discovery...'); + const tiles = await debugInterface.getTiles(); + console.log('✅ Tile discovery completed'); + + console.log('\n3. Testing map metadata...'); + const metadata = await debugInterface.getMetadata(); + console.log('✅ Metadata retrieval completed'); + + console.log('\n4. Testing coordinate conversion...'); + await debugInterface.testCoords(0, 0); + await debugInterface.testCoords(1000, 1000); + console.log('✅ Coordinate conversion completed'); + + console.log('\n5. Attempting map assembly...'); + await debugInterface.assembleMap(); + + } catch (error) { + console.error('\n❌ Detailed Error Analysis:'); + console.error('Error Type:', error.constructor.name); + console.error('Error Message:', error.message); + console.error('Error Stack:', error.stack); + + // Additional debugging info + console.error('\n🔍 Additional Debug Info:'); + console.error('Error Properties:', Object.getOwnPropertyNames(error)); + console.error('Error Code:', error.code); + console.error('Error SQL:', error.sql); + console.error('Error SQL Message:', error.sqlMessage); + } +} + +runDetailedDebug(); diff --git a/backend/debug-map.js b/backend/debug-map.js new file mode 100644 index 0000000..317cff6 --- /dev/null +++ b/backend/debug-map.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +// Debug script for map processing +const debugInterface = require('./server.js'); + +console.log('🔧 Map Processor Debug Console'); +console.log('Available commands:'); +console.log(' assembleMap() - Assemble world map'); +console.log(' getTiles() - Get all map tiles'); +console.log(' getMetadata() - Get map metadata'); +console.log(' testCoords(x, z) - Test coordinate conversion'); +console.log(''); + +// Run assembleMap by default +console.log('🚀 Running map assembly...'); +debugInterface.assembleMap(); diff --git a/backend/map-processor.js b/backend/map-processor.js new file mode 100644 index 0000000..c3667c7 --- /dev/null +++ b/backend/map-processor.js @@ -0,0 +1,248 @@ +const sharp = require('sharp'); +const fs = require('fs').promises; +const path = require('path'); +const { db } = require('./database'); + +class MapProcessor { + constructor() { + this.mapDir = path.join(__dirname, 'uploads', 'map'); + this.outputDir = path.join(__dirname, 'uploads', 'processed'); + this.worldMapPath = path.join(this.outputDir, 'world-map.png'); + this.tileSize = 1024; // Xaero's World Map tiles are 1024x1024 + } + + async init() { + try { + await fs.mkdir(this.outputDir, { recursive: true }); + console.log('Map processor initialized'); + } catch (error) { + console.error('Error initializing map processor:', error); + } + } + + async getMapTiles() { + try { + const files = await fs.readdir(this.mapDir); + const tileFiles = files.filter(file => file.endsWith('.png')); + + // Parse tile coordinates from filenames like "0_3_x-3584_z1536.png" + const tiles = tileFiles.map(file => { + const match = file.match(/(\d+)_(\d+)_x(-?\d+)_z(-?\d+)\.png/); + if (match) { + return { + filename: file, + gridX: parseInt(match[1]), + gridZ: parseInt(match[2]), + x: parseInt(match[3]), + z: parseInt(match[4]) + }; + } + return null; + }).filter(tile => tile !== null); + + return tiles.sort((a, b) => { + // Sort by grid coordinates + if (a.gridX !== b.gridX) return a.gridX - b.gridX; + return a.gridZ - b.gridZ; + }); + } catch (error) { + console.error('Error reading map tiles:', error); + return []; + } + } + + async calculateMapDimensions(tiles) { + if (tiles.length === 0) return { width: 0, height: 0, offsetX: 0, offsetZ: 0 }; + + // Find the center tile (3_1_x-512_z-512.png) + //const centerTile = tiles.find(t => t.filename === '3_1_x-512_z-512.png'); + + let centerX, centerZ; + + //if (centerTile) { + // Use the specified tile as center point + // centerX = centerTile.x + (this.tileSize / 2); // Center of the tile + // centerZ = centerTile.z + (this.tileSize / 2); // Center of the tile + // console.log(`Using tile ${centerTile.filename} as center point at (${centerX}, ${centerZ})`); + //} else { + // Fallback to original logic if center tile not found + centerX = 0; + centerZ = 0; + console.log('Center tile 3_1_x-512_z-512.png not found, using origin (0,0) as center'); + //} + + // Calculate bounds relative to the center + const minX = Math.min(...tiles.map(t => t.x)); + const maxX = Math.max(...tiles.map(t => t.x + this.tileSize)); + const minZ = Math.min(...tiles.map(t => t.z)); + const maxZ = Math.max(...tiles.map(t => t.z + this.tileSize)); + + const width = maxX - minX; + const height = maxZ - minZ; + + // Calculate offsets to make the center tile the origin (0,0) + // We need to shift everything so that the center point becomes (0,0) + const offsetX = -centerX; + const offsetZ = -centerZ; + + return { width, height, offsetX, offsetZ }; + } + + async assembleWorldMap() { + console.log('Starting world map assembly...'); + + const tiles = await this.getMapTiles(); + if (tiles.length === 0) { + console.log('No map tiles found'); + return false; + } + + console.log(`Found ${tiles.length} map tiles`); + + const { width, height, offsetX, offsetZ } = await this.calculateMapDimensions(tiles); + + console.log(`Map dimensions: ${width}x${height}px`); + console.log(`Offset: X=${offsetX}, Z=${offsetZ}`); + + // Create a blank canvas for the world map + const worldMap = sharp({ + create: { + width: width, + height: height, + channels: 4, + background: { r: 0, g: 0, b: 0, alpha: 0 } + } + }); + + // Composite each tile onto the world map + const overlays = await Promise.all(tiles.map(async (tile) => { + const tilePath = path.join(this.mapDir, tile.filename); + const x = tile.x + offsetX; + const y = tile.z + offsetZ; + + return { + input: tilePath, + left: x, + top: y + }; + })); + + try { + await worldMap + .composite(overlays) + .png() + .toFile(this.worldMapPath); + + // Update map metadata in database + await this.updateMapMetadata({ + width: width, + height: height, + offsetX: offsetX, + offsetZ: offsetZ, + tileSize: this.tileSize, + lastUpdated: new Date().toISOString() + }); + + console.log(`World map assembled: ${this.worldMapPath}`); + return true; + } catch (error) { + console.error('Error assembling world map:', error); + return false; + } + } + + async updateMapMetadata(metadata) { + return new Promise((resolve, reject) => { + const keys = Object.keys(metadata); + const values = Object.values(metadata); + + const updatePromises = keys.map((key, index) => { + return new Promise((resolve, reject) => { + db.run( + `INSERT INTO map_metadata (\`key\`, value) VALUES (?, ?) + ON DUPLICATE KEY UPDATE value = VALUES(value)`, + [key, values[index]], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + }); + + Promise.all(updatePromises) + .then(() => resolve()) + .catch(reject); + }); + } + + async getMapMetadata() { + return new Promise((resolve, reject) => { + db.all('SELECT `key`, value FROM map_metadata', [], (err, rows) => { + if (err) reject(err); + else { + const metadata = {}; + rows.forEach(row => { + metadata[row.key] = row.value; + }); + resolve(metadata); + } + }); + }); + } + + async getWorldMapUrl() { + const metadata = await this.getMapMetadata(); + if (metadata.width && metadata.height) { + return '/api/map/world-map'; + } + return null; + } + + async getCoordinateConversion(x, z) { + const metadata = await this.getMapMetadata(); + if (!metadata.width || !metadata.height) { + return { x: 0, y: 0 }; + } + + const offsetX = parseInt(metadata.offsetX) || 0; + const offsetZ = parseInt(metadata.offsetZ) || 0; + const width = parseInt(metadata.width); + const height = parseInt(metadata.height); + + // Convert Minecraft coordinates to pixel coordinates + const pixelX = x + offsetX; + const pixelY = z + offsetZ; + + // Convert to percentage for frontend (0-100%) + const percentX = (pixelX / width) * 100; + const percentY = (pixelY / height) * 100; + + return { + x: percentX, + y: percentY, + pixelX: pixelX, + pixelY: pixelY + }; + } + + async getMarkersWithCoordinates() { + return new Promise((resolve, reject) => { + db.all('SELECT * FROM map_markers WHERE is_public = 1', [], async (err, markers) => { + if (err) reject(err); + else { + const markersWithCoords = await Promise.all(markers.map(async (marker) => { + const coords = await this.getCoordinateConversion(marker.x_coord, marker.z_coord); + return { + ...marker, + coordinates: coords + }; + })); + resolve(markersWithCoords); + } + }); + }); + } +} + +module.exports = MapProcessor; diff --git a/backend/server.js b/backend/server.js index 26759d9..7843b75 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,6 +8,7 @@ const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { db, init } = require('./database'); +const MapProcessor = require('./map-processor'); const app = express(); const PORT = 3000; @@ -1951,6 +1952,396 @@ app.post('/api/data', (req, res) => { const { eventType, player, uuid, timestamp +// Helper function to convert images to WebP +function convertToWebP(inputPath, outputPath) { + return new Promise((resolve, reject) => { + const sharp = require('sharp'); + sharp(inputPath) + .webp({ quality: 80 }) + .toFile(outputPath) + .then(info => { + resolve(info.size); + }) + .catch(reject); + }); +} + +// Initialize Map Processor +const mapProcessor = new MapProcessor(); +mapProcessor.init(); + +// Debug console interface for map processing +// Available when running as standalone script or when required as module +const debugInterface = { + assembleMap: async () => { + try { + console.log('🔄 Starting map assembly...'); + const success = await mapProcessor.assembleWorldMap(); + if (success) { + console.log('✅ Map assembly completed successfully'); + } else { + console.log('❌ Map assembly failed'); + } + } catch (error) { + console.error('❌ Error during map assembly:', error.message); + } + }, + + getTiles: async () => { + try { + const tiles = await mapProcessor.getMapTiles(); + console.log(`📋 Found ${tiles.length} map tiles:`); + tiles.forEach((tile, index) => { + if (index < 5) { // Show first 5 tiles + console.log(` ${tile.filename}: X=${tile.x}, Z=${tile.z}`); + } + }); + if (tiles.length > 5) { + console.log(` ... and ${tiles.length - 5} more tiles`); + } + } catch (error) { + console.error('❌ Error getting tiles:', error.message); + } + }, + + getMetadata: async () => { + try { + const metadata = await mapProcessor.getMapMetadata(); + console.log('📊 Map Metadata:'); + console.log(` Width: ${metadata.width}px`); + console.log(` Height: ${metadata.height}px`); + console.log(` Offset X: ${metadata.offsetX}`); + console.log(` Offset Z: ${metadata.offsetZ}`); + console.log(` Tile Size: ${metadata.tileSize}px`); + console.log(` Last Updated: ${metadata.lastUpdated || 'Never'}`); + } catch (error) { + console.error('❌ Error getting metadata:', error.message); + } + }, + + testCoords: async (x, z) => { + try { + const coords = await mapProcessor.getCoordinateConversion(x, z); + console.log(`📍 Coordinate Conversion for (${x}, ${z}):`); + console.log(` Pixel X: ${coords.pixelX}`); + console.log(` Pixel Y: ${coords.pixelY}`); + console.log(` Percentage X: ${coords.x.toFixed(2)}%`); + console.log(` Percentage Y: ${coords.y.toFixed(2)}%`); + } catch (error) { + console.error('❌ Error converting coordinates:', error.message); + } + } +}; + +// Make debug interface available globally +global.mapProcessor = mapProcessor; +global.assembleMap = debugInterface.assembleMap; +global.getTiles = debugInterface.getTiles; +global.getMetadata = debugInterface.getMetadata; +global.testCoords = debugInterface.testCoords; + +// Export debug interface for module usage +module.exports = debugInterface; + +// If running as standalone script, show console interface +if (require.main === module) { + console.log('🔧 Map Processor Debug Console'); + console.log('Available commands:'); + console.log(' assembleMap() - Assemble world map'); + console.log(' getTiles() - Get all map tiles'); + console.log(' getMetadata() - Get map metadata'); + console.log(' testCoords(x, z) - Test coordinate conversion'); + console.log(''); + console.log('💡 Usage examples:'); + console.log(' node -e "require(\'./server.js\').assembleMap()"'); + console.log(' node -e "require(\'./server.js\').getTiles()"'); + console.log(' node -e "require(\'./server.js\').testCoords(0, 0)"'); + console.log(''); + console.log('🚀 Starting debug session...'); +} + +// === MAP API === + +// Get world map metadata +app.get('/api/map/metadata', (req, res) => { + mapProcessor.getMapMetadata() + .then(metadata => { + res.json({ + width: parseInt(metadata.width) || 0, + height: parseInt(metadata.height) || 0, + offsetX: parseInt(metadata.offsetX) || 0, + offsetZ: parseInt(metadata.offsetZ) || 0, + tileSize: parseInt(metadata.tileSize) || 1024, + lastUpdated: metadata.lastUpdated || null + }); + }) + .catch(err => { + console.error('Error getting map metadata:', err); + res.status(500).json({error: 'Fehler beim Laden der Karten-Metadaten'}); + }); +}); + +// Get assembled world map +app.get('/api/map/world-map', (req, res) => { + const worldMapPath = path.join(__dirname, 'uploads', 'processed', 'world-map.png'); + + if (fs.existsSync(worldMapPath)) { + res.sendFile(worldMapPath); + } else { + res.status(404).json({error: 'Weltkarte nicht gefunden. Bitte zuerst zusammenstellen.'}); + } +}); + +// Assemble world map from tiles +app.post('/api/map/assemble', (req, res) => { + if (!req.isAuthenticated()) return res.status(401).send(); + if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'}); + + console.log('🔄 Starting map assembly process...'); + + // Check if database is ready before assembling + db.get("SELECT 1", [], (err) => { + if (err) { + console.error('❌ Database not ready for map assembly:', err); + return res.status(500).json({ + error: 'Datenbank nicht bereit für Karten-Zusammenstellung', + details: err.message, + stack: err.stack + }); + } + console.log('✅ Database connection verified'); + + mapProcessor.assembleWorldMap() + .then(success => { + if (success) { + console.log('✅ Map assembly completed successfully'); + res.json({success: true, message: 'Weltkarte erfolgreich zusammengestellt'}); + } else { + console.error('❌ Map assembly failed - unknown error'); + res.status(500).json({ + error: 'Karten-Zusammenstellung fehlgeschlagen', + details: 'Unbekannter Fehler bei der Karten-Zusammenstellung' + }); + } + }) + .catch(err => { + console.error('❌ Error assembling world map:', err); + res.status(500).json({ + error: 'Fehler beim Zusammensetzen der Weltkarte', + details: err.message, + stack: err.stack, + type: err.constructor.name + }); + }); + }); +}); + +// Get all markers with coordinates +app.get('/api/map/markers', (req, res) => { + mapProcessor.getMarkersWithCoordinates() + .then(markers => { + res.json(markers); + }) + .catch(err => { + console.error('Error getting markers:', err); + res.status(500).json({error: 'Fehler beim Laden der Marker'}); + }); +}); + +// Get public markers only +app.get('/api/map/markers/public', (req, res) => { + db.all('SELECT * FROM map_markers WHERE is_public = 1', [], async (err, markers) => { + if (err) { + console.error('Error getting public markers:', err); + return res.status(500).json({error: 'Fehler beim Laden der öffentlichen Marker'}); + } + + const markersWithCoords = await Promise.all(markers.map(async (marker) => { + const coords = await mapProcessor.getCoordinateConversion(marker.x_coord, marker.z_coord); + return { + ...marker, + coordinates: coords + }; + })); + + res.json(markersWithCoords); + }); +}); + +// Create new marker (Admin only) +app.post('/api/map/markers', (req, res) => { + if (!req.isAuthenticated()) return res.status(401).send(); + if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'}); + + const { name, type, x_coord, z_coord, description, linked_entity_type, linked_entity_id, icon_type, color } = req.body; + + if (!name || x_coord === undefined || z_coord === undefined) { + return res.status(400).json({error: 'Name und Koordinaten sind erforderlich'}); + } + + // Generate unique ID + const markerId = 'marker_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + + const markerData = { + id: markerId, + name: name.trim(), + type: type || 'poi', + x_coord: parseInt(x_coord), + z_coord: parseInt(z_coord), + description: description || '', + linked_entity_type: linked_entity_type || null, + linked_entity_id: linked_entity_id || null, + icon_type: icon_type || 'flag', + color: color || '#2563eb', + is_public: 1 + }; + + db.run(`INSERT INTO map_markers (id, name, type, x_coord, z_coord, description, linked_entity_type, linked_entity_id, icon_type, color, is_public) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [markerData.id, markerData.name, markerData.type, markerData.x_coord, markerData.z_coord, + markerData.description, markerData.linked_entity_type, markerData.linked_entity_id, + markerData.icon_type, markerData.color, markerData.is_public], + function(err) { + if (err) { + console.error('Error creating marker:', err); + return res.status(500).json({error: 'Fehler beim Erstellen des Markers'}); + } + res.json({ + success: true, + markerId: markerId, + message: 'Marker erfolgreich erstellt' + }); + } + ); +}); + +// Update marker (Admin only) +app.put('/api/map/markers/:markerId', (req, res) => { + if (!req.isAuthenticated()) return res.status(401).send(); + if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'}); + + const { markerId } = req.params; + const updates = req.body; + + // Build dynamic update query + const allowedFields = ['name', 'type', 'x_coord', 'z_coord', 'description', 'linked_entity_type', 'linked_entity_id', 'icon_type', 'color', 'is_public']; + const updateFields = []; + const values = []; + + for (const field of allowedFields) { + if (updates[field] !== undefined) { + updateFields.push(`${field} = ?`); + values.push(field.includes('_coord') ? parseInt(updates[field]) : updates[field]); + } + } + + if (updateFields.length === 0) { + return res.status(400).json({error: 'Keine gültigen Felder zum Aktualisieren'}); + } + + const query = `UPDATE map_markers SET ${updateFields.join(', ')} WHERE id = ?`; + values.push(markerId); + + db.run(query, values, function(err) { + if (err) { + console.error('Error updating marker:', err); + return res.status(500).json({error: 'Fehler beim Aktualisieren des Markers'}); + } + if (this.changes === 0) { + return res.status(404).json({error: 'Marker nicht gefunden'}); + } + res.json({success: true, message: 'Marker erfolgreich aktualisiert'}); + }); +}); + +// Delete marker (Admin only) +app.delete('/api/map/markers/:markerId', (req, res) => { + if (!req.isAuthenticated()) return res.status(401).send(); + if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'}); + + const { markerId } = req.params; + + db.run("DELETE FROM map_markers WHERE id = ?", [markerId], function(err) { + if (err) { + console.error('Error deleting marker:', err); + return res.status(500).json({error: 'Fehler beim Löschen des Markers'}); + } + if (this.changes === 0) { + return res.status(404).json({error: 'Marker nicht gefunden'}); + } + res.json({success: true, message: 'Marker erfolgreich gelöscht'}); + }); +}); + +// Get map layers +app.get('/api/map/layers', (req, res) => { + db.all('SELECT * FROM map_layers ORDER BY order_index', [], (err, rows) => { + if (err) { + console.error('Error getting map layers:', err); + return res.status(500).json({error: 'Fehler beim Laden der Karten-Layer'}); + } + res.json(rows); + }); +}); + +// Update map layer visibility/order (Admin only) +app.put('/api/map/layers/:layerId', (req, res) => { + if (!req.isAuthenticated()) return res.status(401).send(); + if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'}); + + const { layerId } = req.params; + const { is_active, order_index } = req.body; + + const updateFields = []; + const values = []; + + if (is_active !== undefined) { + updateFields.push('is_active = ?'); + values.push(is_active ? 1 : 0); + } + + if (order_index !== undefined) { + updateFields.push('order_index = ?'); + values.push(parseInt(order_index)); + } + + if (updateFields.length === 0) { + return res.status(400).json({error: 'Keine gültigen Felder zum Aktualisieren'}); + } + + const query = `UPDATE map_layers SET ${updateFields.join(', ')} WHERE id = ?`; + values.push(layerId); + + db.run(query, values, function(err) { + if (err) { + console.error('Error updating map layer:', err); + return res.status(500).json({error: 'Fehler beim Aktualisieren des Layers'}); + } + if (this.changes === 0) { + return res.status(404).json({error: 'Layer nicht gefunden'}); + } + res.json({success: true, message: 'Layer erfolgreich aktualisiert'}); + }); +}); + +// Coordinate conversion endpoint +app.get('/api/map/convert-coords', (req, res) => { + const { x, z } = req.query; + + if (x === undefined || z === undefined) { + return res.status(400).json({error: 'X und Z Koordinaten sind erforderlich'}); + } + + mapProcessor.getCoordinateConversion(parseInt(x), parseInt(z)) + .then(coords => { + res.json(coords); + }) + .catch(err => { + console.error('Error converting coordinates:', err); + res.status(500).json({error: 'Fehler bei der Koordinaten-Konvertierung'}); + }); +}); + // Serve uploaded files statically app.use('/uploads', express.static(UPLOAD_DIR)); diff --git a/components/Layout.tsx b/components/Layout.tsx index a04db23..735faab 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -54,7 +54,7 @@ const Layout: React.FC = ({ children, activeTab, onNavigate }) => { {/* Logo */}
onNavigate('dashboard')} + onClick={() => onNavigate('/')} >
P.V. @@ -69,6 +69,7 @@ const Layout: React.FC = ({ children, activeTab, onNavigate }) => { {/* */} + {user?.isAdmin && ( )} @@ -111,6 +112,7 @@ const Layout: React.FC = ({ children, activeTab, onNavigate }) => { setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Bürger setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Organisationen setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Unternehmen + setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Weltkarte {user?.isAdmin && ( setMobileMenuOpen(false)} className="block py-2 text-red-400 hover:text-red-300">Admin )} @@ -182,7 +184,12 @@ const Layout: React.FC = ({ children, activeTab, onNavigate }) => { {/* Links */}
- Dokumentation + Server Status Datenschutz
diff --git a/components/MarkdownEditor.tsx b/components/MarkdownEditor.tsx index c79c536..16d7ba7 100644 --- a/components/MarkdownEditor.tsx +++ b/components/MarkdownEditor.tsx @@ -1,4 +1,6 @@ import React, { useState, useRef } from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import { Icons } from './IconSet'; interface MarkdownEditorProps { @@ -95,15 +97,24 @@ const MarkdownEditor: React.FC = ({ value, onChange, classN ))}
- {/* Textarea */} -