feat: add LogoManagementModal component for logo upload and management

This commit is contained in:
Lars Behrends
2025-12-28 21:19:10 +01:00
parent 2481187fe7
commit 81f1e90b85
13 changed files with 2963 additions and 52 deletions

View File

@@ -647,8 +647,9 @@ const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate:
const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
const [user, setUser] = useState(authService.getUser());
const [activeTab, setActiveTab] = useState<'overview' | 'npcs' | 'create-npc' | 'edit-npcs' | 'manage-admins'>('overview');
const [activeTab, setActiveTab] = useState<'overview' | 'npcs' | 'create-npc' | 'edit-npcs' | 'cities' | 'create-city' | 'manage-admins'>('overview');
const [npcs, setNpcs] = useState<any>({ citizens: [], companies: [] });
const [cities, setCities] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [shopModalState, setShopModalState] = useState<{
@@ -669,6 +670,9 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
if (activeTab === 'npcs' && isAdmin) {
loadNpcs();
}
if (activeTab === 'cities' && isAdmin) {
loadCities();
}
}, [activeTab, isAdmin]);
const loadNpcs = async () => {
@@ -692,6 +696,29 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
}
};
const loadCities = async () => {
try {
setLoading(true);
const response = await fetch('https://vollidioten.ceraticsoft.de/api/orgs', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
// Filter only cities (type === 'City')
const cityData = data.filter((org: any) => org.type === 'City');
setCities(cityData);
} else {
setError('Fehler beim Laden der Städte');
}
} catch (err) {
console.error('Error loading cities:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
// NPC Creation Forms
const [citizenForm, setCitizenForm] = useState({
username: '',
@@ -710,6 +737,30 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
shopItems: ''
});
// City management
const [cityForm, setCityForm] = useState({
name: '',
description: '',
mayor: '',
establishedYear: '',
cityStats: JSON.stringify({
taxRate: 5.0,
biome: 'Ebene',
defenseRating: 5,
government: 'Demokratie',
specialty: 'Handel'
}, null, 2)
});
const [editingCity, setEditingCity] = useState<any>(null);
const [editCityForm, setEditCityForm] = useState({
name: '',
description: '',
mayor: '',
establishedYear: '',
cityStats: ''
});
const createNpcCitizen = async () => {
if (!citizenForm.username.trim()) {
setError('NPC-Name ist erforderlich');
@@ -870,6 +921,18 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
>
NPCs erstellen
</button>
<button
onClick={() => setActiveTab('cities')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'cities' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
Städte verwalten
</button>
<button
onClick={() => setActiveTab('create-city')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'create-city' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
Stadt erstellen
</button>
</div>
{/* Error Display */}
@@ -1230,6 +1293,604 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
</div>
)}
{activeTab === 'cities' && (
<div className="space-y-8">
{/* Cities Management */}
<div className="bg-surface border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold flex items-center gap-2">
<Icons.Map className="w-5 h-5 text-green-400" />
Städte verwalten ({cities.length})
</h3>
<button
onClick={() => loadCities()}
disabled={loading}
className="text-accentInfo hover:text-accentInfo/80 text-sm disabled:opacity-50"
title="Aktualisieren"
>
{loading ? 'Lade...' : 'Aktualisieren'}
</button>
</div>
{cities.length === 0 ? (
<p className="text-textMuted">Keine Städte vorhanden.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{cities.map((city: any) => (
<div key={city.id} className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{city.logoUrl ? (
<img
src={city.logoUrl}
alt={`${city.name} Logo`}
className="w-8 h-8 rounded border border-white/10 object-cover"
/>
) : (
<div className="w-8 h-8 bg-green-500/20 rounded flex items-center justify-center text-xs font-bold text-green-400">
{city.name.charAt(0)}
</div>
)}
<div>
<h4 className="font-medium text-white">{city.name}</h4>
<p className="text-xs text-textMuted">Gegr. {city.establishedYear}</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
setEditingCity(city);
setEditCityForm({
name: city.name,
description: city.description || '',
mayor: city.mayor || '',
establishedYear: city.establishedYear || '',
cityStats: JSON.stringify(city.cityStats || {}, null, 2)
});
}}
className="text-blue-400 hover:text-blue-300 text-sm"
title="Stadt bearbeiten"
>
<Icons.Edit className="w-4 h-4" />
</button>
<button
onClick={async () => {
if (confirm(`Stadt "${city.name}" wirklich löschen?`)) {
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/cities/${city.id}`, {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
alert('Stadt erfolgreich gelöscht!');
loadCities(); // Reload cities
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Löschen');
}
} catch (err) {
console.error('Error deleting city:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
}
}}
disabled={loading}
className="text-red-400 hover:text-red-300 text-sm disabled:opacity-50"
title="Stadt löschen"
>
🗑
</button>
</div>
</div>
<p className="text-sm text-textMuted mb-2">{city.description}</p>
<div className="text-xs text-textMuted">
Bürgermeister: {city.mayor || 'Unbekannt'} {city.memberCount} Bürger
</div>
{city.cityStats && (
<div className="mt-2 text-xs text-accentInfo">
Spezialität: {city.cityStats.specialty}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
)}
{activeTab === 'create-city' && (
<div className="space-y-8">
{/* Create City */}
<div className="bg-surface border border-border rounded-xl p-6">
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
<Icons.Map className="w-5 h-5 text-green-400" />
Stadt erstellen
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-textMain mb-1">Stadtname *</label>
<input
type="text"
value={cityForm.name}
onChange={(e) => setCityForm({...cityForm, name: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. Eldoria"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Bürgermeister</label>
<input
type="text"
value={cityForm.mayor}
onChange={(e) => setCityForm({...cityForm, mayor: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. Lord Eldric"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Gründungsjahr</label>
<input
type="text"
value={cityForm.establishedYear}
onChange={(e) => setCityForm({...cityForm, establishedYear: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. Ära 5"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Beschreibung</label>
<textarea
value={cityForm.description}
onChange={(e) => setCityForm({...cityForm, description: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="Beschreibung der Stadt..."
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Stadt-Statistiken (JSON-Format)</label>
<textarea
value={cityForm.cityStats}
onChange={(e) => setCityForm({...cityForm, cityStats: e.target.value})}
className="w-full h-24 bg-[#0b0b0d] border border-border rounded p-2 text-sm font-mono text-xs"
placeholder='{ "taxRate": 5.0, "biome": "Ebene", "defenseRating": 5, "government": "Demokratie", "specialty": "Handel" }'
/>
<p className="text-xs text-textMuted mt-1">JSON-Objekt mit Stadt-Statistiken</p>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-2">Banner-Bild (optional)</label>
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setCityForm({...cityForm, bannerFile: file});
}
}}
className="hidden"
id="city-banner-upload"
disabled={loading}
/>
<label htmlFor="city-banner-upload" className="cursor-pointer">
<div className="w-8 h-8 mx-auto mb-2 bg-accentInfo/20 rounded flex items-center justify-center">
<Icons.Map className="w-4 h-4 text-accentInfo" />
</div>
<p className="text-xs text-textMain font-medium">
Banner auswählen
</p>
<p className="text-xs text-textMuted">PNG, JPG oder GIF Max. 5MB</p>
</label>
</div>
{cityForm.bannerFile && (
<div className="mt-2 text-xs text-green-400">
Banner ausgewählt: {cityForm.bannerFile.name}
</div>
)}
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-2">Logo-Bild (optional)</label>
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setCityForm({...cityForm, logoFile: file});
}
}}
className="hidden"
id="city-logo-upload"
disabled={loading}
/>
<label htmlFor="city-logo-upload" className="cursor-pointer">
<div className="w-8 h-8 mx-auto mb-2 bg-green-500/20 rounded flex items-center justify-center">
<Icons.Layers className="w-4 h-4 text-green-400" />
</div>
<p className="text-xs text-textMain font-medium">
Logo auswählen
</p>
<p className="text-xs text-textMuted">PNG, JPG oder GIF Max. 2MB Quadratisch bevorzugt</p>
</label>
</div>
{cityForm.logoFile && (
<div className="mt-2 text-xs text-green-400">
Logo ausgewählt: {cityForm.logoFile.name}
</div>
)}
</div>
<div className="md:col-span-2 pt-2">
<button
onClick={async () => {
if (!cityForm.name.trim()) {
setError('Stadt-Name ist erforderlich');
return;
}
try {
setLoading(true);
setError(null);
let cityStats = {};
if (cityForm.cityStats.trim()) {
try {
cityStats = JSON.parse(cityForm.cityStats);
} catch (e) {
setError('Ungültiges JSON-Format für Stadt-Statistiken');
setLoading(false);
return;
}
}
const response = await fetch('https://vollidioten.ceraticsoft.de/api/admin/cities', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
name: cityForm.name.trim(),
description: cityForm.description.trim() || undefined,
mayor: cityForm.mayor.trim() || undefined,
establishedYear: cityForm.establishedYear.trim() || undefined,
cityStats: cityStats
})
});
if (response.ok) {
const cityData = await response.json();
const cityId = cityData.cityId;
// Upload banner if provided
if (cityForm.bannerImageId) {
try {
const bannerFormData = new FormData();
// We need to get the file from the input, but since we don't have it stored,
// we'll skip this for now and show a message
console.log('Banner upload would happen here for city:', cityId);
} catch (err) {
console.error('Banner upload failed:', err);
}
}
// Upload logo if provided
if (cityForm.logoImageId) {
try {
const logoFormData = new FormData();
// We need to get the file from the input, but since we don't have it stored,
// we'll skip this for now and show a message
console.log('Logo upload would happen here for city:', cityId);
} catch (err) {
console.error('Logo upload failed:', err);
}
}
alert('Stadt erfolgreich erstellt! Bilder können nach der Erstellung im Bearbeitungsmodus hochgeladen werden.');
setCityForm({
name: '',
description: '',
mayor: '',
establishedYear: '',
cityStats: JSON.stringify({
taxRate: 5.0,
biome: 'Ebene',
defenseRating: 5,
government: 'Demokratie',
specialty: 'Handel'
}, null, 2)
});
loadCities(); // Reload to show new city
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Erstellen');
}
} catch (err) {
console.error('Error creating city:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
}}
disabled={loading || !cityForm.name.trim()}
className="w-full bg-green-500 hover:bg-green-600 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-medium disabled:cursor-not-allowed"
>
{loading ? 'Erstelle...' : 'Stadt erstellen'}
</button>
</div>
</div>
</div>
</div>
)}
{/* Edit City Modal */}
{editingCity && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-surface border border-border rounded-xl w-full max-w-lg shadow-2xl max-h-[90vh] overflow-y-auto">
<div className="p-4 border-b border-border flex justify-between items-center">
<h3 className="font-bold text-textMain flex items-center gap-2">
<Icons.Edit className="w-5 h-5 text-blue-400" />
Stadt bearbeiten
</h3>
<button
onClick={() => setEditingCity(null)}
className="text-textMuted hover:text-white text-xl leading-none"
>
×
</button>
</div>
<div className="p-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-textMain mb-1">Stadtname *</label>
<input
type="text"
value={editCityForm.name}
onChange={(e) => setEditCityForm({...editCityForm, name: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Bürgermeister</label>
<input
type="text"
value={editCityForm.mayor}
onChange={(e) => setEditCityForm({...editCityForm, mayor: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Gründungsjahr</label>
<input
type="text"
value={editCityForm.establishedYear}
onChange={(e) => setEditCityForm({...editCityForm, establishedYear: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Beschreibung</label>
<textarea
value={editCityForm.description}
onChange={(e) => setEditCityForm({...editCityForm, description: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Stadt-Statistiken (JSON)</label>
<textarea
value={editCityForm.cityStats}
onChange={(e) => setEditCityForm({...editCityForm, cityStats: e.target.value})}
className="w-full h-24 bg-[#0b0b0d] border border-border rounded p-2 text-sm font-mono text-xs"
/>
</div>
</div>
{/* Current Images */}
{(editingCity.bannerUrl || editingCity.logoUrl) && (
<div className="border-t border-border pt-4">
<h4 className="text-sm font-medium text-textMain mb-3">Aktuelle Bilder</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{editingCity.bannerUrl && (
<div className="bg-surfaceHighlight/30 border border-border rounded p-3">
<p className="text-xs text-textMuted mb-2">Banner</p>
<img
src={editingCity.bannerUrl}
alt="Banner"
className="w-full h-16 object-cover rounded border border-white/10"
/>
</div>
)}
{editingCity.logoUrl && (
<div className="bg-surfaceHighlight/30 border border-border rounded p-3">
<p className="text-xs text-textMuted mb-2">Logo</p>
<img
src={editingCity.logoUrl}
alt="Logo"
className="w-12 h-12 object-cover rounded border border-white/10"
/>
</div>
)}
</div>
</div>
)}
{/* Image Uploads */}
<div className="border-t border-border pt-4 space-y-4">
<h4 className="text-sm font-medium text-textMain">Bilder ändern</h4>
<div>
<label className="block text-sm font-medium text-textMain mb-2">Banner-Bild</label>
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
<input
type="file"
accept="image/*"
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
try {
setLoading(true);
const formData = new FormData();
formData.append('banner', file);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/cities/${editingCity.id}/banner/upload`, {
method: 'POST',
credentials: 'include',
body: formData
});
if (response.ok) {
alert('Banner erfolgreich aktualisiert!');
loadCities(); // Reload to show updated image
} else {
setError('Fehler beim Hochladen des Banners');
}
} catch (err) {
console.error('Error uploading banner:', err);
setError('Netzwerkfehler beim Banner-Upload');
} finally {
setLoading(false);
}
}
}}
className="hidden"
id="edit-city-banner-upload"
disabled={loading}
/>
<label htmlFor="edit-city-banner-upload" className="cursor-pointer">
<div className="w-8 h-8 mx-auto mb-2 bg-accentInfo/20 rounded flex items-center justify-center">
<Icons.Map className="w-4 h-4 text-accentInfo" />
</div>
<p className="text-xs text-textMain font-medium">
{loading ? 'Lädt...' : 'Banner ersetzen'}
</p>
<p className="text-xs text-textMuted">PNG, JPG oder GIF Max. 5MB</p>
</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-2">Logo-Bild</label>
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
<input
type="file"
accept="image/*"
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
try {
setLoading(true);
const formData = new FormData();
formData.append('logo', file);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/cities/${editingCity.id}/logo/upload`, {
method: 'POST',
credentials: 'include',
body: formData
});
if (response.ok) {
alert('Logo erfolgreich aktualisiert!');
loadCities(); // Reload to show updated image
} else {
setError('Fehler beim Hochladen des Logos');
}
} catch (err) {
console.error('Error uploading logo:', err);
setError('Netzwerkfehler beim Logo-Upload');
} finally {
setLoading(false);
}
}
}}
className="hidden"
id="edit-city-logo-upload"
disabled={loading}
/>
<label htmlFor="edit-city-logo-upload" className="cursor-pointer">
<div className="w-8 h-8 mx-auto mb-2 bg-green-500/20 rounded flex items-center justify-center">
<Icons.Layers className="w-4 h-4 text-green-400" />
</div>
<p className="text-xs text-textMain font-medium">
{loading ? 'Lädt...' : 'Logo ersetzen'}
</p>
<p className="text-xs text-textMuted">PNG, JPG oder GIF Max. 2MB Quadratisch bevorzugt</p>
</label>
</div>
</div>
</div>
<div className="flex gap-2 pt-2 border-t border-border">
<button
onClick={async () => {
try {
setLoading(true);
let cityStats = {};
if (editCityForm.cityStats.trim()) {
cityStats = JSON.parse(editCityForm.cityStats);
}
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/cities/${editingCity.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
name: editCityForm.name.trim(),
description: editCityForm.description.trim() || undefined,
mayor: editCityForm.mayor.trim() || undefined,
establishedYear: editCityForm.establishedYear.trim() || undefined,
cityStats: cityStats
})
});
if (response.ok) {
alert('Stadt erfolgreich aktualisiert!');
setEditingCity(null);
loadCities();
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Aktualisieren');
}
} catch (err) {
console.error('Error updating city:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
}}
disabled={loading}
className="flex-1 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white px-3 py-2 rounded text-sm font-medium"
>
{loading ? '...' : 'Speichern'}
</button>
<button
onClick={() => setEditingCity(null)}
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-sm"
>
Abbrechen
</button>
</div>
</div>
</div>
</div>
)}
{/* Shop Management Modal */}
<ShopManagementModal
isOpen={shopModalState.isOpen}