mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
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
This commit is contained in:
696
pages/AdminMapManagement.tsx
Normal file
696
pages/AdminMapManagement.tsx
Normal file
@@ -0,0 +1,696 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { MapMetadata, MapLayer, MapMarker } from '../types';
|
||||
import { authService } from '../services/AuthService';
|
||||
import { DiscordUser } from '../types';
|
||||
|
||||
const AdminMapManagement: React.FC = () => {
|
||||
const [user, setUser] = useState<DiscordUser | null>(null);
|
||||
const [mapMetadata, setMapMetadata] = useState<MapMetadata | null>(null);
|
||||
const [layers, setLayers] = useState<MapLayer[]>([]);
|
||||
const [markers, setMarkers] = useState<MapMarker[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Map assembly state
|
||||
const [isAssembling, setIsAssembling] = useState(false);
|
||||
const [assemblyProgress, setAssemblyProgress] = useState<string>('');
|
||||
const [assemblyLog, setAssemblyLog] = useState<string[]>([]);
|
||||
|
||||
// Layer management state
|
||||
const [editingLayer, setEditingLayer] = useState<MapLayer | null>(null);
|
||||
const [newLayer, setNewLayer] = useState({ name: '', is_active: true });
|
||||
|
||||
// Marker management state
|
||||
const [editingMarker, setEditingMarker] = useState<MapMarker | null>(null);
|
||||
const [newMarker, setNewMarker] = useState({
|
||||
name: '',
|
||||
type: 'poi' as const,
|
||||
x_coord: 0,
|
||||
z_coord: 0,
|
||||
description: '',
|
||||
linked_entity_type: null as string | null,
|
||||
linked_entity_id: null as number | null,
|
||||
icon_type: 'flag' as const,
|
||||
color: '#2563eb',
|
||||
is_public: true
|
||||
});
|
||||
|
||||
// Subscribe to auth changes
|
||||
useEffect(() => {
|
||||
const unsub = authService.subscribe((currentUser) => {
|
||||
setUser(currentUser);
|
||||
});
|
||||
return unsub;
|
||||
}, []);
|
||||
|
||||
// Load map data
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
if (!user?.isAdmin) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Load map metadata
|
||||
try {
|
||||
const metadataResponse = await fetch('/api/map/metadata');
|
||||
if (metadataResponse.ok) {
|
||||
const metadata = await metadataResponse.json();
|
||||
setMapMetadata(metadata);
|
||||
}
|
||||
} catch (metaErr) {
|
||||
console.log('Map metadata not available:', metaErr);
|
||||
}
|
||||
|
||||
// Load layers
|
||||
const layersResponse = await fetch('/api/map/layers');
|
||||
if (layersResponse.ok) {
|
||||
const layerData = await layersResponse.json();
|
||||
setLayers(layerData);
|
||||
}
|
||||
|
||||
// Load all markers (including private ones)
|
||||
const markersResponse = await fetch('/api/map/markers');
|
||||
if (markersResponse.ok) {
|
||||
const markerData = await markersResponse.json();
|
||||
setMarkers(markerData);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading map data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [user?.isAdmin]);
|
||||
|
||||
// Map assembly function
|
||||
const handleAssembleMap = async () => {
|
||||
if (!user?.isAdmin || isAssembling) return;
|
||||
|
||||
setIsAssembling(true);
|
||||
setAssemblyProgress('Starte Karten-Zusammenstellung...');
|
||||
setAssemblyLog(['Starte Karten-Zusammenstellung...']);
|
||||
|
||||
try {
|
||||
setAssemblyProgress('Überprüfe Kacheln...');
|
||||
addToLog('Überprüfe Kacheln...');
|
||||
|
||||
// Check if tiles exist
|
||||
const tilesResponse = await fetch('/api/map/metadata');
|
||||
if (!tilesResponse.ok) {
|
||||
setAssemblyProgress('Keine Kacheln gefunden. Bitte stellen Sie sicher, dass PNG-Dateien im Ordner backend/uploads/map/ vorhanden sind.');
|
||||
addToLog('❌ Keine Kacheln gefunden');
|
||||
setTimeout(() => {
|
||||
setIsAssembling(false);
|
||||
setAssemblyProgress('');
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
setAssemblyProgress('Starte Karten-Zusammenstellung...');
|
||||
addToLog('Starte Karten-Zusammenstellung...');
|
||||
|
||||
const response = await fetch('/api/map/assemble', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Server-Fehler: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setAssemblyProgress('Karte erfolgreich zusammengestellt!');
|
||||
addToLog('✅ Karte erfolgreich zusammengestellt!');
|
||||
|
||||
// Reload map metadata
|
||||
const metadataResponse = await fetch('/api/map/metadata');
|
||||
if (metadataResponse.ok) {
|
||||
const metadata = await metadataResponse.json();
|
||||
setMapMetadata(metadata);
|
||||
}
|
||||
|
||||
setIsAssembling(false);
|
||||
setAssemblyProgress('');
|
||||
alert('Weltkarte wurde erfolgreich zusammengestellt!');
|
||||
} else {
|
||||
throw new Error(result.message || 'Fehler beim Zusammensetzen der Karte');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error assembling map:', err);
|
||||
setAssemblyProgress(`Fehler: ${err.message}`);
|
||||
addToLog(`❌ Fehler: ${err.message}`);
|
||||
setTimeout(() => {
|
||||
setIsAssembling(false);
|
||||
setAssemblyProgress('');
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const addToLog = (message: string) => {
|
||||
setAssemblyLog(prev => [...prev, message]);
|
||||
};
|
||||
|
||||
// Layer management functions
|
||||
const handleCreateLayer = async () => {
|
||||
if (!user?.isAdmin) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/map/layers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newLayer)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setLayers([...layers, result]);
|
||||
setNewLayer({ name: '', is_active: true });
|
||||
} else {
|
||||
throw new Error('Fehler beim Erstellen des Layers');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating layer:', err);
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateLayer = async (layerId: string) => {
|
||||
if (!user?.isAdmin || !editingLayer) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/map/layers/${layerId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(editingLayer)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setLayers(layers.map(l => l.id === layerId ? result : l));
|
||||
setEditingLayer(null);
|
||||
} else {
|
||||
throw new Error('Fehler beim Aktualisieren des Layers');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating layer:', err);
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteLayer = async (layerId: string) => {
|
||||
if (!user?.isAdmin) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/map/layers/${layerId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setLayers(layers.filter(l => l.id !== layerId));
|
||||
} else {
|
||||
throw new Error('Fehler beim Löschen des Layers');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting layer:', err);
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler');
|
||||
}
|
||||
};
|
||||
|
||||
// Marker management functions
|
||||
const handleCreateMarker = async () => {
|
||||
if (!user?.isAdmin) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/map/markers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newMarker)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setMarkers([...markers, result]);
|
||||
setNewMarker({
|
||||
name: '',
|
||||
type: 'poi',
|
||||
x_coord: 0,
|
||||
z_coord: 0,
|
||||
description: '',
|
||||
linked_entity_type: null,
|
||||
linked_entity_id: null,
|
||||
icon_type: 'flag',
|
||||
color: '#2563eb',
|
||||
is_public: true
|
||||
});
|
||||
} else {
|
||||
throw new Error('Fehler beim Erstellen des Markers');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating marker:', err);
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateMarker = async (markerId: string) => {
|
||||
if (!user?.isAdmin || !editingMarker) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/map/markers/${markerId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(editingMarker)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setMarkers(markers.map(m => m.id === markerId ? result : m));
|
||||
setEditingMarker(null);
|
||||
} else {
|
||||
throw new Error('Fehler beim Aktualisieren des Markers');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating marker:', err);
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteMarker = async (markerId: string) => {
|
||||
if (!user?.isAdmin) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/map/markers/${markerId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setMarkers(markers.filter(m => m.id !== markerId));
|
||||
} else {
|
||||
throw new Error('Fehler beim Löschen des Markers');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting marker:', err);
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler');
|
||||
}
|
||||
};
|
||||
|
||||
if (!user?.isAdmin) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-red-500">Zugriff verweigert: Admin-Berechtigungen erforderlich</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-textMuted">Lade Map-Management...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-textPrimary">Karten-Management</h1>
|
||||
<div className="text-sm text-textMuted">
|
||||
Admin-Tools für Weltkarte, Marker und Layer
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/50 text-red-500 p-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map Assembly Section */}
|
||||
<div className="bg-bgSecondary border border-border rounded-lg p-6">
|
||||
<h2 className="text-lg font-semibold text-textPrimary mb-4">Karten-Zusammenstellung</h2>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Assembly Controls */}
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-bgPrimary rounded">
|
||||
<h3 className="font-semibold text-textPrimary mb-2">Karten-Status</h3>
|
||||
{mapMetadata ? (
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="text-textMuted">Status: <span className="text-green-400 font-medium">Zusammengestellt</span></div>
|
||||
<div className="text-textMuted">Größe: <span className="text-textPrimary">{mapMetadata.width} x {mapMetadata.height}px</span></div>
|
||||
<div className="text-textMuted">Offset: <span className="text-textPrimary">X: {mapMetadata.offsetX}, Z: {mapMetadata.offsetZ}</span></div>
|
||||
<div className="text-textMuted">Letztes Update: <span className="text-textPrimary">{new Date().toLocaleString()}</span></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-textMuted">Status: <span className="text-red-400 font-medium">Nicht zusammengestellt</span></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={handleAssembleMap}
|
||||
disabled={isAssembling}
|
||||
className="w-full px-4 py-2 bg-accentSuccess text-white rounded hover:bg-accentSuccess/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isAssembling ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>{assemblyProgress}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span>{mapMetadata ? 'Karte neu zusammensetzen' : 'Karte zusammensetzen'}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="text-xs text-textMuted">
|
||||
{mapMetadata
|
||||
? 'Aktualisiert die bestehende Weltkarte mit neuen Kacheln aus backend/uploads/map/'
|
||||
: 'Erstellt die Weltkarte aus den PNG-Kacheln im Upload-Ordner'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Assembly Log */}
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-bgPrimary rounded">
|
||||
<h3 className="font-semibold text-textPrimary mb-2">Zusammenstellungs-Log</h3>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{assemblyLog.length === 0 ? (
|
||||
<div className="text-textMuted text-sm">Keine Aktivitäten</div>
|
||||
) : (
|
||||
assemblyLog.map((log, index) => (
|
||||
<div key={index} className="text-sm font-mono text-textMuted">
|
||||
{log}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-bgPrimary rounded">
|
||||
<h3 className="font-semibold text-textPrimary mb-2">Anweisungen</h3>
|
||||
<div className="text-sm text-textMuted space-y-1">
|
||||
<div>1. Stellen Sie sicher, dass PNG-Kacheln im Ordner <code className="bg-bgSecondary px-1 rounded">backend/uploads/map/</code> im Backend-Container vorhanden sind</div>
|
||||
<div>2. Klicken Sie auf "Karte zusammensetzen"</div>
|
||||
<div>3. Warten Sie, bis der Vorgang abgeschlossen ist</div>
|
||||
<div>4. Die Karte ist dann unter <code className="bg-bgSecondary px-1 rounded">/world-map</code> verfügbar</div>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-textMuted border-t border-border pt-2">
|
||||
<strong>Hinweis:</strong> Wenn "Keine Kacheln gefunden" angezeigt wird, befolgen Sie bitte die Anleitung in <code className="bg-bgSecondary px-1 rounded">MAP_SETUP_GUIDE.md</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layer Management */}
|
||||
<div className="bg-bgSecondary border border-border rounded-lg p-6">
|
||||
<h2 className="text-lg font-semibold text-textPrimary mb-4">Layer-Verwaltung</h2>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Create Layer */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-textPrimary">Neuen Layer erstellen</h3>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Layer-Name"
|
||||
value={newLayer.name}
|
||||
onChange={(e) => setNewLayer({...newLayer, name: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newLayer.is_active}
|
||||
onChange={(e) => setNewLayer({...newLayer, is_active: e.target.checked})}
|
||||
/>
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
<button
|
||||
onClick={handleCreateLayer}
|
||||
className="px-4 py-2 bg-accentInfo text-white rounded hover:bg-accentInfo/90"
|
||||
>
|
||||
Layer erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layer List */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-textPrimary">Bestehende Layer</h3>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
{layers.length === 0 ? (
|
||||
<div className="text-textMuted text-sm">Keine Layer vorhanden</div>
|
||||
) : (
|
||||
layers.map(layer => (
|
||||
<div key={layer.id} className="flex items-center justify-between p-3 bg-bgPrimary rounded">
|
||||
<div>
|
||||
<div className="font-medium text-textPrimary">{layer.name}</div>
|
||||
<div className="text-xs text-textMuted">
|
||||
{layer.is_active ? 'Aktiv' : 'Inaktiv'} • ID: {layer.id}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setEditingLayer(layer)}
|
||||
className="px-2 py-1 bg-bgTertiary text-textPrimary rounded text-sm hover:bg-bgPrimary"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteLayer(layer.id)}
|
||||
className="px-2 py-1 bg-red-500/20 text-red-400 rounded text-sm hover:bg-red-500/30"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit Layer Modal */}
|
||||
{editingLayer && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
|
||||
<div className="bg-bgSecondary border border-border rounded-lg p-6 w-full max-w-md">
|
||||
<h3 className="text-lg font-semibold text-textPrimary mb-4">Layer bearbeiten</h3>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={editingLayer.name}
|
||||
onChange={(e) => setEditingLayer({...editingLayer, name: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingLayer.is_active}
|
||||
onChange={(e) => setEditingLayer({...editingLayer, is_active: e.target.checked})}
|
||||
/>
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleUpdateLayer(editingLayer.id)}
|
||||
className="flex-1 px-4 py-2 bg-accentInfo text-white rounded hover:bg-accentInfo/90"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingLayer(null)}
|
||||
className="flex-1 px-4 py-2 bg-bgTertiary text-textPrimary rounded hover:bg-bgPrimary"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Marker Management */}
|
||||
<div className="bg-bgSecondary border border-border rounded-lg p-6">
|
||||
<h2 className="text-lg font-semibold text-textPrimary mb-4">Marker-Verwaltung</h2>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Create Marker */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-textPrimary">Neuen Marker erstellen</h3>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={newMarker.name}
|
||||
onChange={(e) => setNewMarker({...newMarker, name: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="X-Koordinate"
|
||||
value={newMarker.x_coord}
|
||||
onChange={(e) => setNewMarker({...newMarker, x_coord: parseInt(e.target.value)})}
|
||||
className="px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Z-Koordinate"
|
||||
value={newMarker.z_coord}
|
||||
onChange={(e) => setNewMarker({...newMarker, z_coord: parseInt(e.target.value)})}
|
||||
className="px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Beschreibung"
|
||||
value={newMarker.description}
|
||||
onChange={(e) => setNewMarker({...newMarker, description: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
value={newMarker.color}
|
||||
onChange={(e) => setNewMarker({...newMarker, color: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreateMarker}
|
||||
className="w-full px-4 py-2 bg-accentInfo text-white rounded hover:bg-accentInfo/90"
|
||||
>
|
||||
Marker erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Marker List */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-textPrimary">Bestehende Marker</h3>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{markers.length === 0 ? (
|
||||
<div className="text-textMuted text-sm">Keine Marker vorhanden</div>
|
||||
) : (
|
||||
markers.map(marker => (
|
||||
<div key={marker.id} className="flex items-center justify-between p-3 bg-bgPrimary rounded">
|
||||
<div>
|
||||
<div className="font-medium text-textPrimary">{marker.name}</div>
|
||||
<div className="text-xs text-textMuted">
|
||||
{marker.type} • {marker.x_coord}, {marker.z_coord} • {marker.is_public ? 'Öffentlich' : 'Privat'}
|
||||
</div>
|
||||
{marker.description && (
|
||||
<div className="text-xs text-textMuted mt-1">{marker.description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setEditingMarker(marker)}
|
||||
className="px-2 py-1 bg-bgTertiary text-textPrimary rounded text-sm hover:bg-bgPrimary"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteMarker(marker.id)}
|
||||
className="px-2 py-1 bg-red-500/20 text-red-400 rounded text-sm hover:bg-red-500/30"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit Marker Modal */}
|
||||
{editingMarker && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
|
||||
<div className="bg-bgSecondary border border-border rounded-lg p-6 w-full max-w-md">
|
||||
<h3 className="text-lg font-semibold text-textPrimary mb-4">Marker bearbeiten</h3>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={editingMarker.name}
|
||||
onChange={(e) => setEditingMarker({...editingMarker, name: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<input
|
||||
type="number"
|
||||
value={editingMarker.x_coord}
|
||||
onChange={(e) => setEditingMarker({...editingMarker, x_coord: parseInt(e.target.value)})}
|
||||
className="px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={editingMarker.z_coord}
|
||||
onChange={(e) => setEditingMarker({...editingMarker, z_coord: parseInt(e.target.value)})}
|
||||
className="px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={editingMarker.description}
|
||||
onChange={(e) => setEditingMarker({...editingMarker, description: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
value={editingMarker.color}
|
||||
onChange={(e) => setEditingMarker({...editingMarker, color: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bgPrimary border border-border rounded"
|
||||
/>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingMarker.is_public === 1}
|
||||
onChange={(e) => setEditingMarker({...editingMarker, is_public: e.target.checked ? 1 : 0})}
|
||||
/>
|
||||
<span>Öffentlich anzeigen</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleUpdateMarker(editingMarker.id)}
|
||||
className="flex-1 px-4 py-2 bg-accentInfo text-white rounded hover:bg-accentInfo/90"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingMarker(null)}
|
||||
className="flex-1 px-4 py-2 bg-bgTertiary text-textPrimary rounded hover:bg-bgPrimary"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminMapManagement;
|
||||
Reference in New Issue
Block a user