From 065a6e657d425272b543c97851cd20a329b70384 Mon Sep 17 00:00:00 2001 From: Lars Behrends Date: Fri, 2 Jan 2026 05:08:07 +0100 Subject: [PATCH] feat: add world map functionality and admin map management - Added world map page with interactive marker display - Implemented admin map management for marker CRUD operations - Added map layers and markers seed data to database - Integrated new routes for map functionality - Updated database configuration for production environment - Added documentation page route - Enhanced package.json with required dependencies for map features --- App.tsx | 12 + MAP_SETUP_GUIDE.md | 90 + backend/database.js | 91 +- backend/debug-map-detailed.js | 52 + backend/debug-map.js | 16 + backend/map-processor.js | 248 +++ backend/server.js | 391 +++++ components/Layout.tsx | 11 +- components/MarkdownEditor.tsx | 29 +- docker-compose.yml | 11 +- package-lock.json | 1489 ++++++++++++++++- package.json | 2 + pages/Admin.tsx | 14 +- pages/AdminMapManagement.tsx | 696 ++++++++ pages/CityProfile.tsx | 39 + pages/Dokumentation.tsx | 469 ++++++ pages/EditMarker.tsx | 366 ++++ pages/PlayerProfile.tsx | 88 +- pages/WorldMap.tsx | 724 ++++++++ .../advancement/advancement_categories.png | Bin 0 -> 1070 bytes .../assets/advancement/advancement_groups.png | Bin 0 -> 618 bytes .../advancement/adventure/adventure_time.png | Bin 0 -> 923 bytes .../advancement/adventure/arbalistic.png | Bin 0 -> 1684 bytes .../advancement/adventure/avoid_vibration.png | Bin 0 -> 814 bytes .../assets/advancement/adventure/blowback.png | Bin 0 -> 1058 bytes .../advancement/adventure/brush_armadillo.png | Bin 0 -> 1018 bytes .../assets/advancement/adventure/bullseye.png | Bin 0 -> 912 bytes .../craft_decorated_pot_using_only_sherds.png | Bin 0 -> 1095 bytes .../adventure/crafters_crafting_crafters.png | Bin 0 -> 1043 bytes .../adventure/fall_from_world_height.png | Bin 0 -> 1043 bytes .../adventure/heart_transplanter.png | Bin 0 -> 1013 bytes .../adventure/hero_of_the_village.png | Bin 0 -> 1154 bytes .../adventure/honey_block_slide.png | Bin 0 -> 939 bytes .../advancement/adventure/kill_a_mob.png | Bin 0 -> 1049 bytes .../advancement/adventure/kill_all_mobs.png | Bin 0 -> 935 bytes .../kill_mob_near_sculk_catalyst.png | Bin 0 -> 800 bytes .../advancement/adventure/lighten_up.png | Bin 0 -> 908 bytes .../lightning_rod_with_villager_no_fire.png | Bin 0 -> 1020 bytes .../adventure/minecraft_trials_edition.png | Bin 0 -> 2081 bytes .../assets/advancement/adventure/ol_betsy.png | Bin 0 -> 989 bytes .../advancement/adventure/overoverkill.png | Bin 0 -> 721 bytes .../adventure/play_jukebox_in_meadows.png | Bin 0 -> 858 bytes .../read_power_from_chiseled_bookshelf.png | Bin 0 -> 982 bytes .../advancement/adventure/revaulting.png | Bin 0 -> 1349 bytes public/assets/advancement/adventure/root.png | Bin 0 -> 775 bytes .../advancement/adventure/salvage_sherd.png | Bin 0 -> 938 bytes .../advancement/adventure/shoot_arrow.png | Bin 0 -> 986 bytes .../advancement/adventure/sleep_in_bed.png | Bin 0 -> 531 bytes .../advancement/adventure/sniper_duel.png | Bin 0 -> 944 bytes .../advancement/adventure/spear_many_mobs.png | Bin 0 -> 1782 bytes .../adventure/spyglass_at_dragon.png | Bin 0 -> 810 bytes .../adventure/spyglass_at_ghast.png | Bin 0 -> 856 bytes .../adventure/spyglass_at_parrot.png | Bin 0 -> 839 bytes .../adventure/summon_iron_golem.png | Bin 0 -> 1123 bytes .../advancement/adventure/throw_trident.png | Bin 0 -> 1404 bytes .../adventure/totem_of_undying.png | Bin 0 -> 962 bytes public/assets/advancement/adventure/trade.png | Bin 0 -> 1078 bytes .../adventure/trade_at_world_height.png | Bin 0 -> 1745 bytes ...trim_with_all_exclusive_armor_patterns.png | Bin 0 -> 1589 bytes .../adventure/trim_with_any_armor_pattern.png | Bin 0 -> 1054 bytes .../adventure/two_birds_one_arrow.png | Bin 0 -> 1138 bytes .../adventure/under_lock_and_key.png | Bin 0 -> 1104 bytes .../advancement/adventure/use_lodestone.png | Bin 0 -> 788 bytes .../adventure/very_very_frightening.png | Bin 0 -> 889 bytes .../advancement/adventure/voluntary_exile.png | Bin 0 -> 902 bytes ...walk_on_powder_snow_with_leather_boots.png | Bin 0 -> 961 bytes .../adventure/who_needs_rockets.png | Bin 0 -> 869 bytes .../adventure/whos_the_pillager_now.png | Bin 0 -> 956 bytes .../assets/advancement/end/dragon_breath.png | Bin 0 -> 810 bytes public/assets/advancement/end/dragon_egg.png | Bin 0 -> 721 bytes public/assets/advancement/end/elytra.png | Bin 0 -> 799 bytes .../advancement/end/enter_end_gateway.png | Bin 0 -> 654 bytes .../assets/advancement/end/find_end_city.png | Bin 0 -> 798 bytes public/assets/advancement/end/kill_dragon.png | Bin 0 -> 1081 bytes public/assets/advancement/end/levitate.png | Bin 0 -> 973 bytes .../assets/advancement/end/respawn_dragon.png | Bin 0 -> 880 bytes public/assets/advancement/end/root.png | Bin 0 -> 581 bytes .../allay_deliver_cake_to_note_block.png | Bin 0 -> 754 bytes .../allay_deliver_item_to_player.png | Bin 0 -> 943 bytes .../husbandry/axolotl_in_a_bucket.png | Bin 0 -> 828 bytes .../advancement/husbandry/balanced_diet.png | Bin 0 -> 648 bytes .../husbandry/breed_all_animals.png | Bin 0 -> 5446 bytes .../advancement/husbandry/breed_an_animal.png | Bin 0 -> 711 bytes .../husbandry/complete_catalogue.png | Bin 0 -> 2086 bytes .../advancement/husbandry/feed_snifflet.png | Bin 0 -> 937 bytes .../advancement/husbandry/fishy_business.png | Bin 0 -> 827 bytes .../advancement/husbandry/froglights.png | Bin 0 -> 711 bytes .../husbandry/kill_axolotl_target.png | Bin 0 -> 739 bytes .../husbandry/leash_all_frog_variants.png | Bin 0 -> 783 bytes .../husbandry/make_a_sign_glow.png | Bin 0 -> 926 bytes .../advancement/husbandry/netherite_hoe.png | Bin 0 -> 945 bytes .../husbandry/obtain_sniffer_egg.png | Bin 0 -> 726 bytes .../husbandry/place_dried_ghast_in_water.png | Bin 0 -> 509 bytes .../husbandry/plant_any_sniffer_seed.png | Bin 0 -> 827 bytes .../advancement/husbandry/plant_seed.png | Bin 0 -> 849 bytes .../husbandry/remove_wolf_armor.png | Bin 0 -> 968 bytes .../husbandry/repair_wolf_armor.png | Bin 0 -> 1031 bytes .../husbandry/ride_a_boat_with_a_goat.png | Bin 0 -> 771 bytes public/assets/advancement/husbandry/root.png | Bin 0 -> 761 bytes .../husbandry/safely_harvest_honey.png | Bin 0 -> 1338 bytes .../advancement/husbandry/silk_touch_nest.png | Bin 0 -> 1018 bytes .../husbandry/tactical_fishing.png | Bin 0 -> 909 bytes .../husbandry/tadpole_in_a_bucket.png | Bin 0 -> 751 bytes .../advancement/husbandry/tame_an_animal.png | Bin 0 -> 872 bytes .../assets/advancement/husbandry/wax_off.png | Bin 0 -> 848 bytes .../assets/advancement/husbandry/wax_on.png | Bin 0 -> 923 bytes .../advancement/husbandry/whole_pack.png | Bin 0 -> 1939 bytes .../assets/advancement/nether/all_effects.png | Bin 0 -> 1124 bytes .../assets/advancement/nether/all_potions.png | Bin 0 -> 879 bytes .../assets/advancement/nether/brew_potion.png | Bin 0 -> 880 bytes .../nether/charge_respawn_anchor.png | Bin 0 -> 888 bytes .../advancement/nether/create_beacon.png | Bin 0 -> 914 bytes .../advancement/nether/create_full_beacon.png | Bin 0 -> 992 bytes .../advancement/nether/distract_piglin.png | Bin 0 -> 541 bytes .../advancement/nether/explore_nether.png | Bin 0 -> 838 bytes .../assets/advancement/nether/fast_travel.png | Bin 0 -> 710 bytes .../advancement/nether/find_bastion.png | Bin 0 -> 846 bytes .../advancement/nether/find_fortress.png | Bin 0 -> 674 bytes .../advancement/nether/get_wither_skull.png | Bin 0 -> 734 bytes .../advancement/nether/loot_bastion.png | Bin 0 -> 1295 bytes .../advancement/nether/netherite_armor.png | Bin 0 -> 769 bytes .../nether/obtain_ancient_debris.png | Bin 0 -> 876 bytes .../advancement/nether/obtain_blaze_rod.png | Bin 0 -> 1068 bytes .../nether/obtain_crying_obsidian.png | Bin 0 -> 716 bytes .../advancement/nether/return_to_sender.png | Bin 0 -> 1042 bytes .../advancement/nether/ride_strider.png | Bin 0 -> 1173 bytes .../nether/ride_strider_in_overworld_lava.png | Bin 0 -> 1176 bytes public/assets/advancement/nether/root.png | Bin 0 -> 505 bytes .../advancement/nether/summon_wither.png | Bin 0 -> 620 bytes .../advancement/nether/uneasy_alliance.png | Bin 0 -> 739 bytes .../advancement/nether/use_lodestone.png | Bin 0 -> 737 bytes .../story/cure_zombie_villager.png | Bin 0 -> 1003 bytes .../advancement/story/deflect_arrow.png | Bin 0 -> 877 bytes .../assets/advancement/story/enchant_item.png | Bin 0 -> 839 bytes .../advancement/story/enter_the_end.png | Bin 0 -> 1707 bytes .../advancement/story/enter_the_nether.png | Bin 0 -> 638 bytes .../advancement/story/follow_ender_eye.png | Bin 0 -> 776 bytes .../advancement/story/form_obsidian.png | Bin 0 -> 815 bytes .../assets/advancement/story/iron_tools.png | Bin 0 -> 1017 bytes .../assets/advancement/story/lava_bucket.png | Bin 0 -> 788 bytes .../assets/advancement/story/mine_diamond.png | Bin 0 -> 893 bytes .../assets/advancement/story/mine_stone.png | Bin 0 -> 761 bytes .../assets/advancement/story/obtain_armor.png | Bin 0 -> 693 bytes public/assets/advancement/story/root.png | Bin 0 -> 908 bytes .../assets/advancement/story/shiny_gear.png | Bin 0 -> 659 bytes .../assets/advancement/story/smelt_iron.png | Bin 0 -> 714 bytes .../advancement/story/upgrade_tools.png | Bin 0 -> 930 bytes services/AuthService.ts | 6 +- test-api-connection.js | 63 + test-database-connection.js | 55 + test-map-assembly.js | 56 + types.ts | 41 + 152 files changed, 5024 insertions(+), 35 deletions(-) create mode 100644 MAP_SETUP_GUIDE.md create mode 100644 backend/debug-map-detailed.js create mode 100644 backend/debug-map.js create mode 100644 backend/map-processor.js create mode 100644 pages/AdminMapManagement.tsx create mode 100644 pages/Dokumentation.tsx create mode 100644 pages/EditMarker.tsx create mode 100644 pages/WorldMap.tsx create mode 100644 public/assets/advancement/advancement_categories.png create mode 100644 public/assets/advancement/advancement_groups.png create mode 100644 public/assets/advancement/adventure/adventure_time.png create mode 100644 public/assets/advancement/adventure/arbalistic.png create mode 100644 public/assets/advancement/adventure/avoid_vibration.png create mode 100644 public/assets/advancement/adventure/blowback.png create mode 100644 public/assets/advancement/adventure/brush_armadillo.png create mode 100644 public/assets/advancement/adventure/bullseye.png create mode 100644 public/assets/advancement/adventure/craft_decorated_pot_using_only_sherds.png create mode 100644 public/assets/advancement/adventure/crafters_crafting_crafters.png create mode 100644 public/assets/advancement/adventure/fall_from_world_height.png create mode 100644 public/assets/advancement/adventure/heart_transplanter.png create mode 100644 public/assets/advancement/adventure/hero_of_the_village.png create mode 100644 public/assets/advancement/adventure/honey_block_slide.png create mode 100644 public/assets/advancement/adventure/kill_a_mob.png create mode 100644 public/assets/advancement/adventure/kill_all_mobs.png create mode 100644 public/assets/advancement/adventure/kill_mob_near_sculk_catalyst.png create mode 100644 public/assets/advancement/adventure/lighten_up.png create mode 100644 public/assets/advancement/adventure/lightning_rod_with_villager_no_fire.png create mode 100644 public/assets/advancement/adventure/minecraft_trials_edition.png create mode 100644 public/assets/advancement/adventure/ol_betsy.png create mode 100644 public/assets/advancement/adventure/overoverkill.png create mode 100644 public/assets/advancement/adventure/play_jukebox_in_meadows.png create mode 100644 public/assets/advancement/adventure/read_power_from_chiseled_bookshelf.png create mode 100644 public/assets/advancement/adventure/revaulting.png create mode 100644 public/assets/advancement/adventure/root.png create mode 100644 public/assets/advancement/adventure/salvage_sherd.png create mode 100644 public/assets/advancement/adventure/shoot_arrow.png create mode 100644 public/assets/advancement/adventure/sleep_in_bed.png create mode 100644 public/assets/advancement/adventure/sniper_duel.png create mode 100644 public/assets/advancement/adventure/spear_many_mobs.png create mode 100644 public/assets/advancement/adventure/spyglass_at_dragon.png create mode 100644 public/assets/advancement/adventure/spyglass_at_ghast.png create mode 100644 public/assets/advancement/adventure/spyglass_at_parrot.png create mode 100644 public/assets/advancement/adventure/summon_iron_golem.png create mode 100644 public/assets/advancement/adventure/throw_trident.png create mode 100644 public/assets/advancement/adventure/totem_of_undying.png create mode 100644 public/assets/advancement/adventure/trade.png create mode 100644 public/assets/advancement/adventure/trade_at_world_height.png create mode 100644 public/assets/advancement/adventure/trim_with_all_exclusive_armor_patterns.png create mode 100644 public/assets/advancement/adventure/trim_with_any_armor_pattern.png create mode 100644 public/assets/advancement/adventure/two_birds_one_arrow.png create mode 100644 public/assets/advancement/adventure/under_lock_and_key.png create mode 100644 public/assets/advancement/adventure/use_lodestone.png create mode 100644 public/assets/advancement/adventure/very_very_frightening.png create mode 100644 public/assets/advancement/adventure/voluntary_exile.png create mode 100644 public/assets/advancement/adventure/walk_on_powder_snow_with_leather_boots.png create mode 100644 public/assets/advancement/adventure/who_needs_rockets.png create mode 100644 public/assets/advancement/adventure/whos_the_pillager_now.png create mode 100644 public/assets/advancement/end/dragon_breath.png create mode 100644 public/assets/advancement/end/dragon_egg.png create mode 100644 public/assets/advancement/end/elytra.png create mode 100644 public/assets/advancement/end/enter_end_gateway.png create mode 100644 public/assets/advancement/end/find_end_city.png create mode 100644 public/assets/advancement/end/kill_dragon.png create mode 100644 public/assets/advancement/end/levitate.png create mode 100644 public/assets/advancement/end/respawn_dragon.png create mode 100644 public/assets/advancement/end/root.png create mode 100644 public/assets/advancement/husbandry/allay_deliver_cake_to_note_block.png create mode 100644 public/assets/advancement/husbandry/allay_deliver_item_to_player.png create mode 100644 public/assets/advancement/husbandry/axolotl_in_a_bucket.png create mode 100644 public/assets/advancement/husbandry/balanced_diet.png create mode 100644 public/assets/advancement/husbandry/breed_all_animals.png create mode 100644 public/assets/advancement/husbandry/breed_an_animal.png create mode 100644 public/assets/advancement/husbandry/complete_catalogue.png create mode 100644 public/assets/advancement/husbandry/feed_snifflet.png create mode 100644 public/assets/advancement/husbandry/fishy_business.png create mode 100644 public/assets/advancement/husbandry/froglights.png create mode 100644 public/assets/advancement/husbandry/kill_axolotl_target.png create mode 100644 public/assets/advancement/husbandry/leash_all_frog_variants.png create mode 100644 public/assets/advancement/husbandry/make_a_sign_glow.png create mode 100644 public/assets/advancement/husbandry/netherite_hoe.png create mode 100644 public/assets/advancement/husbandry/obtain_sniffer_egg.png create mode 100644 public/assets/advancement/husbandry/place_dried_ghast_in_water.png create mode 100644 public/assets/advancement/husbandry/plant_any_sniffer_seed.png create mode 100644 public/assets/advancement/husbandry/plant_seed.png create mode 100644 public/assets/advancement/husbandry/remove_wolf_armor.png create mode 100644 public/assets/advancement/husbandry/repair_wolf_armor.png create mode 100644 public/assets/advancement/husbandry/ride_a_boat_with_a_goat.png create mode 100644 public/assets/advancement/husbandry/root.png create mode 100644 public/assets/advancement/husbandry/safely_harvest_honey.png create mode 100644 public/assets/advancement/husbandry/silk_touch_nest.png create mode 100644 public/assets/advancement/husbandry/tactical_fishing.png create mode 100644 public/assets/advancement/husbandry/tadpole_in_a_bucket.png create mode 100644 public/assets/advancement/husbandry/tame_an_animal.png create mode 100644 public/assets/advancement/husbandry/wax_off.png create mode 100644 public/assets/advancement/husbandry/wax_on.png create mode 100644 public/assets/advancement/husbandry/whole_pack.png create mode 100644 public/assets/advancement/nether/all_effects.png create mode 100644 public/assets/advancement/nether/all_potions.png create mode 100644 public/assets/advancement/nether/brew_potion.png create mode 100644 public/assets/advancement/nether/charge_respawn_anchor.png create mode 100644 public/assets/advancement/nether/create_beacon.png create mode 100644 public/assets/advancement/nether/create_full_beacon.png create mode 100644 public/assets/advancement/nether/distract_piglin.png create mode 100644 public/assets/advancement/nether/explore_nether.png create mode 100644 public/assets/advancement/nether/fast_travel.png create mode 100644 public/assets/advancement/nether/find_bastion.png create mode 100644 public/assets/advancement/nether/find_fortress.png create mode 100644 public/assets/advancement/nether/get_wither_skull.png create mode 100644 public/assets/advancement/nether/loot_bastion.png create mode 100644 public/assets/advancement/nether/netherite_armor.png create mode 100644 public/assets/advancement/nether/obtain_ancient_debris.png create mode 100644 public/assets/advancement/nether/obtain_blaze_rod.png create mode 100644 public/assets/advancement/nether/obtain_crying_obsidian.png create mode 100644 public/assets/advancement/nether/return_to_sender.png create mode 100644 public/assets/advancement/nether/ride_strider.png create mode 100644 public/assets/advancement/nether/ride_strider_in_overworld_lava.png create mode 100644 public/assets/advancement/nether/root.png create mode 100644 public/assets/advancement/nether/summon_wither.png create mode 100644 public/assets/advancement/nether/uneasy_alliance.png create mode 100644 public/assets/advancement/nether/use_lodestone.png create mode 100644 public/assets/advancement/story/cure_zombie_villager.png create mode 100644 public/assets/advancement/story/deflect_arrow.png create mode 100644 public/assets/advancement/story/enchant_item.png create mode 100644 public/assets/advancement/story/enter_the_end.png create mode 100644 public/assets/advancement/story/enter_the_nether.png create mode 100644 public/assets/advancement/story/follow_ender_eye.png create mode 100644 public/assets/advancement/story/form_obsidian.png create mode 100644 public/assets/advancement/story/iron_tools.png create mode 100644 public/assets/advancement/story/lava_bucket.png create mode 100644 public/assets/advancement/story/mine_diamond.png create mode 100644 public/assets/advancement/story/mine_stone.png create mode 100644 public/assets/advancement/story/obtain_armor.png create mode 100644 public/assets/advancement/story/root.png create mode 100644 public/assets/advancement/story/shiny_gear.png create mode 100644 public/assets/advancement/story/smelt_iron.png create mode 100644 public/assets/advancement/story/upgrade_tools.png create mode 100644 test-api-connection.js create mode 100644 test-database-connection.js create mode 100644 test-map-assembly.js 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 */} -