Files
project_vollidioten_website/pages/Admin.tsx

1895 lines
77 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { Icons } from '../components/IconSet';
import { authService } from '../services/AuthService';
import NpcBannerManagementModal from '../components/NpcBannerManagementModal';
import NpcLogoManagementModal from '../components/NpcLogoManagementModal';
import NpcGalleryManagementModal from '../components/NpcGalleryManagementModal';
interface AdminPageProps {
onBack: () => void;
}
const EditNpcCitizenCard: React.FC<{ citizen: any; onUpdate: () => void; onError: (error: string) => void }> = ({ citizen, onUpdate, onError }) => {
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({
username: citizen.username,
tags: citizen.tags.join(', '),
role: citizen.stats.role,
organizationId: citizen.stats.organizationId || '',
storyMarkdown: citizen.storyMarkdown || ''
});
const [loading, setLoading] = useState(false);
const handleSave = async () => {
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/players/${citizen.uuid}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
tags: formData.tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0),
organizationId: formData.organizationId || null,
storyMarkdown: formData.storyMarkdown.trim() || null
})
});
if (response.ok) {
setIsEditing(false);
onUpdate();
} else {
const errorData = await response.json();
onError(errorData.error || response.json());
}
} catch (err) {
console.error('Error updating NPC citizen:', err);
onError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
if (isEditing) {
return (
<div className="bg-surfaceHighlight/50 border border-border rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Icons.Edit className="w-4 h-4 text-blue-400" />
<span className="font-medium text-white">{citizen.username} bearbeiten</span>
</div>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-textMain mb-1">Tags</label>
<input
type="text"
value={formData.tags}
onChange={(e) => setFormData({...formData, tags: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
placeholder="#Bürger, #Händler"
/>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Rolle</label>
<select
value={formData.role}
onChange={(e) => setFormData({...formData, role: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
<option value="Bürger">Bürger</option>
<option value="Händler">Händler</option>
<option value="Schmied">Schmied</option>
<option value="Bauer">Bauer</option>
<option value="Wächter">Wächter</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Stadt</label>
<select
value={formData.organizationId}
onChange={(e) => setFormData({...formData, organizationId: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
<option value="">Keine Stadt</option>
<option value="org-3">Provisorium Null</option>
<option value="org-4">Sakura</option>
</select>
</div>
<div className="flex gap-2 pt-2">
<button
onClick={handleSave}
disabled={loading}
className="flex-1 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white px-3 py-1 rounded text-xs font-medium"
>
{loading ? '...' : 'Speichern'}
</button>
<button
onClick={() => setIsEditing(false)}
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-xs"
>
Abbrechen
</button>
</div>
</div>
</div>
);
}
return (
<div 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">
<div className="w-8 h-8 bg-purple-500/20 rounded flex items-center justify-center text-xs font-bold text-purple-400">
NPC
</div>
<div>
<h4 className="font-medium text-white">{citizen.username}</h4>
<p className="text-xs text-textMuted">{citizen.stats.role}</p>
</div>
</div>
<button
onClick={() => setIsEditing(true)}
className="text-blue-400 hover:text-blue-300 text-sm"
>
<Icons.Edit className="w-4 h-4" />
</button>
</div>
<div className="text-xs text-textMuted">
Level {citizen.stats.level} {citizen.stats.playtimeHours}h Spielzeit
</div>
<div className="text-xs text-textMuted mt-1">
Tags: {citizen.tags.join(', ') || 'Keine'}
</div>
</div>
);
};
const ShopManagementModal: React.FC<{
isOpen: boolean;
onClose: () => void;
shopItems: any[];
onUpdate: (items: any[]) => void;
projectId: string;
onError: (error: string) => void;
}> = ({ isOpen, onClose, shopItems, onUpdate, projectId, onError }) => {
const [localItems, setLocalItems] = useState<any[]>(shopItems || []);
const [editingItem, setEditingItem] = useState<any>(null);
const [isAdding, setIsAdding] = useState(false);
const [loading, setLoading] = useState(false);
const resetForm = () => {
setEditingItem(null);
setIsAdding(false);
};
const handleSaveItem = async (itemData: any) => {
try {
setLoading(true);
let updatedItems = [...localItems];
if (editingItem) {
// Update existing item
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop/${editingItem.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(itemData)
});
if (response.ok) {
const index = updatedItems.findIndex(item => item.id === editingItem.id);
if (index !== -1) {
updatedItems[index] = itemData;
}
} else {
throw new Error('Fehler beim Aktualisieren');
}
} else {
// Add new item
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(itemData)
});
if (response.ok) {
const newItem = await response.json();
updatedItems.push(newItem.item);
} else {
throw new Error('Fehler beim Hinzufügen');
}
}
setLocalItems(updatedItems);
onUpdate(updatedItems);
resetForm();
} catch (err) {
console.error('Error saving shop item:', err);
onError('Fehler beim Speichern des Artikels');
} finally {
setLoading(false);
}
};
const handleDeleteItem = async (itemId: string) => {
if (!confirm('Artikel wirklich löschen?')) return;
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop/${itemId}`, {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
const updatedItems = localItems.filter(item => item.id !== itemId);
setLocalItems(updatedItems);
onUpdate(updatedItems);
} else {
throw new Error('Fehler beim Löschen');
}
} catch (err) {
console.error('Error deleting shop item:', err);
onError('Fehler beim Löschen des Artikels');
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-surface border border-border rounded-xl max-w-4xl w-full max-h-[80vh] overflow-hidden">
<div className="p-6 border-b border-border">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Icons.ShoppingBag className="w-5 h-5 text-accentInfo" />
Shop-Verwaltung
</h2>
<button onClick={onClose} className="text-textMuted hover:text-white">
×
</button>
</div>
</div>
<div className="p-6 overflow-y-auto max-h-[60vh]">
<div className="flex justify-between items-center mb-6">
<div>
<h3 className="font-semibold text-white">Shop-Artikel ({localItems.length})</h3>
<p className="text-xs text-textMuted">Verwalte die Produkte dieses Shops</p>
</div>
<button
onClick={() => setIsAdding(true)}
className="bg-accentInfo hover:bg-accentInfo/80 text-white px-4 py-2 rounded text-sm font-medium flex items-center gap-2"
>
+
Artikel hinzufügen
</button>
</div>
{localItems.length === 0 ? (
<div className="text-center py-12 text-textMuted">
<Icons.ShoppingBag className="w-12 h-12 mx-auto mb-4 opacity-20" />
<p>Noch keine Artikel im Shop.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{localItems.map((item) => (
<div key={item.id} className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
<div className="flex justify-between items-start mb-3">
<div>
<h4 className="font-medium text-white">{item.name}</h4>
<p className="text-xs text-textMuted">{item.description}</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setEditingItem(item)}
className="text-blue-400 hover:text-blue-300 text-sm"
>
<Icons.Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteItem(item.id)}
className="text-red-400 hover:text-red-300 text-sm"
>
🗑
</button>
</div>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-accentInfo font-medium">
{item.price} {item.currency}
</span>
<span className="text-textMuted">
Lager: {item.stock}
</span>
</div>
</div>
))}
</div>
)}
{(editingItem || isAdding) && (
<ShopItemForm
item={editingItem}
onSave={handleSaveItem}
onCancel={resetForm}
loading={loading}
/>
)}
</div>
</div>
</div>
);
};
const ShopItemForm: React.FC<{
item?: any;
onSave: (itemData: any) => void;
onCancel: () => void;
loading: boolean;
}> = ({ item, onSave, onCancel, loading }) => {
const [formData, setFormData] = useState({
name: item?.name || '',
description: item?.description || '',
price: item?.price || '',
currency: item?.currency || 'Gold',
stock: item?.stock || '',
type: item?.type || 'item'
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave({
...formData,
price: parseFloat(formData.price),
stock: parseInt(formData.stock),
id: item?.id // Keep existing ID for updates
});
};
return (
<div className="bg-surfaceHighlight/50 border border-border rounded-lg p-4">
<h4 className="font-medium text-white mb-4">
{item ? 'Artikel bearbeiten' : 'Neuer Artikel'}
</h4>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-textMain mb-1">Name *</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
required
/>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Typ</label>
<select
value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="item">Artikel</option>
<option value="service">Dienstleistung</option>
<option value="resource">Ressource</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Preis *</label>
<input
type="number"
step="0.01"
value={formData.price}
onChange={(e) => setFormData({...formData, price: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
required
/>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Währung</label>
<select
value={formData.currency}
onChange={(e) => setFormData({...formData, currency: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="Gold">Gold</option>
<option value="Diamanten">Diamanten</option>
<option value="Eisen">Eisen</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Lagerbestand *</label>
<input
type="number"
value={formData.stock}
onChange={(e) => setFormData({...formData, stock: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
required
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Beschreibung</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
/>
</div>
<div className="flex gap-2 pt-2">
<button
type="submit"
disabled={loading}
className="flex-1 bg-accentInfo hover:bg-accentInfo/80 disabled:opacity-50 text-white px-3 py-2 rounded text-sm font-medium"
>
{loading ? '...' : (item ? 'Speichern' : 'Hinzufügen')}
</button>
<button
type="button"
onClick={onCancel}
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-sm"
>
Abbrechen
</button>
</div>
</form>
</div>
);
};
const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate: () => void; onError: (error: string) => void; onOpenShopModal: (projectId: string, shopItems: any[]) => void }> = ({ company, npcCitizens, onUpdate, onError, onOpenShopModal }) => {
const [isEditing, setIsEditing] = useState(false);
const [isEditingShop, setIsEditingShop] = useState(false);
const [isManaging, setIsManaging] = useState(false);
const [shopItems, setShopItems] = useState<any[]>(company.shopCatalog || []);
const [bannerModalOpen, setBannerModalOpen] = useState(false);
const [logoModalOpen, setLogoModalOpen] = useState(false);
const [galleryModalOpen, setGalleryModalOpen] = useState(false);
const [formData, setFormData] = useState({
title: company.title,
description: company.description || '',
category: company.category,
owner: company.owner,
associatedOrgId: company.associatedOrgId || '',
shopItems: JSON.stringify(company.shopCatalog || [], null, 2)
});
const [loading, setLoading] = useState(false);
const handleShopUpdate = (updatedItems: any[]) => {
setShopItems(updatedItems);
setFormData(prev => ({
...prev,
shopItems: JSON.stringify(updatedItems, null, 2)
}));
};
const handleSave = async () => {
try {
setLoading(true);
// Parse shop items
let shopCatalog = [];
if (formData.shopItems.trim()) {
try {
shopCatalog = JSON.parse(formData.shopItems);
} catch (e) {
onError('Ungültiges JSON-Format für Shop-Artikel');
setLoading(false);
return;
}
}
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${company.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title: formData.title,
description: formData.description,
category: formData.category,
associatedOrgId: formData.associatedOrgId || null,
shopCatalog: shopCatalog
})
});
if (response.ok) {
setIsEditing(false);
onUpdate();
} else {
const errorData = await response.json();
onError(errorData.error || 'Fehler beim Aktualisieren');
}
} catch (err) {
console.error('Error updating NPC company:', err);
onError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
if (isEditing) {
return (
<div className="bg-surfaceHighlight/50 border border-border rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Icons.Edit className="w-4 h-4 text-purple-400" />
<span className="font-medium text-white">{company.title} bearbeiten</span>
</div>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-textMain mb-1">Name</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
/>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Kategorie</label>
<select
value={formData.category}
onChange={(e) => setFormData({...formData, category: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
<option value="Enterprise">Enterprise</option>
<option value="Service">Service</option>
<option value="Story Arc">Story Arc</option>
<option value="Black Market">Black Market</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Eigentümer</label>
<select
value={formData.owner}
onChange={(e) => setFormData({...formData, owner: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
{npcCitizens.map((citizen: any) => (
<option key={citizen.uuid} value={citizen.username}>
{citizen.username}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Stadt</label>
<select
value={formData.associatedOrgId}
onChange={(e) => setFormData({...formData, associatedOrgId: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-xs"
>
<option value="">Keine Stadt</option>
<option value="org-3">Provisorium Null</option>
<option value="org-4">Sakura</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-textMain mb-1">Beschreibung</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
className="w-full h-16 bg-[#0b0b0d] border border-border rounded p-2 text-xs"
/>
</div>
<div className="flex gap-2 pt-2">
<button
onClick={handleSave}
disabled={loading}
className="flex-1 bg-purple-500 hover:bg-purple-600 disabled:opacity-50 text-white px-3 py-1 rounded text-xs font-medium"
>
{loading ? '...' : 'Speichern'}
</button>
<button
onClick={() => setIsEditing(false)}
className="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded text-xs"
>
Abbrechen
</button>
</div>
</div>
</div>
);
}
return (
<>
<div 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">
<div className="w-8 h-8 bg-purple-500/20 rounded flex items-center justify-center text-xs font-bold text-purple-400">
NPC
</div>
<div>
<h4 className="font-medium text-white">{company.title}</h4>
<p className="text-xs text-textMuted">{company.category}</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => setBannerModalOpen(true)}
className="text-blue-400 hover:text-blue-300 text-sm"
title="Banner bearbeiten"
>
<Icons.Layers className="w-4 h-4" />
</button>
<button
onClick={() => setLogoModalOpen(true)}
className="text-green-400 hover:text-green-300 text-sm"
title="Logo bearbeiten"
>
<Icons.Shield className="w-4 h-4" />
</button>
<button
onClick={() => setGalleryModalOpen(true)}
className="text-orange-400 hover:text-orange-300 text-sm"
title="Portfolio verwalten"
>
<Icons.Box className="w-4 h-4" />
</button>
<button
onClick={() => onOpenShopModal(company.id, company.shopCatalog || [])}
className="text-accentInfo hover:text-accentInfo/80 text-sm"
title="Shop verwalten"
>
<Icons.ShoppingBag className="w-4 h-4" />
</button>
<button
onClick={() => setIsEditing(true)}
className="text-purple-400 hover:text-purple-300 text-sm"
>
<Icons.Edit className="w-4 h-4" />
</button>
</div>
</div>
<div className="text-xs text-textMuted">
Eigentümer: {company.owner}
</div>
{company.shopCatalog && company.shopCatalog.length > 0 && (
<div className="text-xs text-accentInfo mt-1">
Shop: {company.shopCatalog.length} Artikel
</div>
)}
</div>
{/* Image Management Modals */}
<NpcBannerManagementModal
isOpen={bannerModalOpen}
onClose={() => setBannerModalOpen(false)}
projectId={company.id}
currentBannerUrl={company.bannerUrl}
onUpdate={() => onUpdate()}
/>
<NpcLogoManagementModal
isOpen={logoModalOpen}
onClose={() => setLogoModalOpen(false)}
projectId={company.id}
currentLogoUrl={company.logoUrl}
onUpdate={() => onUpdate()}
/>
<NpcGalleryManagementModal
isOpen={galleryModalOpen}
onClose={() => setGalleryModalOpen(false)}
projectId={company.id}
onUpdate={() => onUpdate()}
/>
</>
);
};
const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
const [user, setUser] = useState(authService.getUser());
const [activeTab, setActiveTab] = useState<'overview' | '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<{
isOpen: boolean;
projectId: string;
shopItems: any[];
}>({ isOpen: false, projectId: '', shopItems: [] });
// Check if user is admin
const isAdmin = user?.isAdmin;
useEffect(() => {
const unsub = authService.subscribe(setUser);
return unsub;
}, []);
useEffect(() => {
loadNpcs();
loadCities();
}, [isAdmin]);
// Auto-refresh data every 30 seconds when on relevant tabs
useEffect(() => {
if (!isAdmin) return;
const interval = setInterval(() => {
if (activeTab === 'edit-npcs' || activeTab === 'create-npc') {
loadNpcs();
} else if (activeTab === 'cities' || activeTab === 'create-city') {
loadCities();
}
}, 30000); // 30 seconds
return () => clearInterval(interval);
}, [activeTab, isAdmin]);
const loadNpcs = async () => {
try {
setLoading(true);
const response = await fetch('https://vollidioten.ceraticsoft.de/api/admin/npcs', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
setNpcs(data);
} else {
setError('Fehler beim Laden der NPCs');
}
} catch (err) {
console.error('Error loading NPCs:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
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: '',
tags: '',
role: 'Bürger',
organizationId: '',
storyMarkdown: ''
});
const [companyForm, setCompanyForm] = useState({
title: '',
description: '',
category: 'Enterprise',
owner: '',
associatedOrgId: '',
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');
return;
}
try {
setLoading(true);
setError(null);
const response = await fetch('https://vollidioten.ceraticsoft.de/api/admin/npc-citizen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
username: citizenForm.username.trim(),
tags: citizenForm.tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0),
role: citizenForm.role,
organizationId: citizenForm.organizationId || null,
storyMarkdown: citizenForm.storyMarkdown.trim() || undefined
})
});
if (response.ok) {
alert('NPC-Bürger erfolgreich erstellt!');
setCitizenForm({
username: '',
tags: '',
role: 'Bürger',
organizationId: '',
storyMarkdown: ''
});
if (activeTab === 'npcs') loadNpcs();
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Erstellen');
}
} catch (err) {
console.error('Error creating NPC citizen:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
const createNpcCompany = async () => {
if (!companyForm.title.trim() || !companyForm.owner.trim()) {
setError('Titel und NPC-Eigentümer sind erforderlich');
return;
}
try {
setLoading(true);
setError(null);
// Parse shop items if provided
let shopCatalog = [];
if (companyForm.shopItems.trim()) {
try {
shopCatalog = JSON.parse(companyForm.shopItems);
} catch (e) {
setError('Ungültiges JSON-Format für Shop-Artikel');
setLoading(false);
return;
}
}
const response = await fetch('https://vollidioten.ceraticsoft.de/api/admin/npc-company', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title: companyForm.title.trim(),
description: companyForm.description.trim() || undefined,
category: companyForm.category,
owner: companyForm.owner.trim(),
associatedOrgId: companyForm.associatedOrgId || null,
shopCatalog: shopCatalog
})
});
if (response.ok) {
alert('NPC-Firma erfolgreich erstellt!');
setCompanyForm({
title: '',
description: '',
category: 'Enterprise',
owner: '',
associatedOrgId: '',
shopItems: ''
});
if (activeTab === 'npcs') loadNpcs();
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Erstellen');
}
} catch (err) {
console.error('Error creating NPC company:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
if (!isAdmin) {
return (
<div className="max-w-4xl mx-auto animate-in slide-in-from-right-4 duration-300">
<div className="flex justify-between items-center mb-6">
<button onClick={onBack} className="flex items-center gap-2 text-sm text-textMuted hover:text-textMain transition-colors">
<span className="text-lg"></span> Zurück
</button>
</div>
<div className="bg-surface border border-border rounded-xl p-8 text-center">
<Icons.Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-white mb-2">Admin-Berechtigung erforderlich</h2>
<p className="text-textMuted">Sie haben keine Berechtigung, auf die Admin-Funktionen zuzugreifen.</p>
</div>
</div>
);
}
return (
<div className="max-w-6xl mx-auto animate-in slide-in-from-right-4 duration-300">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold mb-1">Admin-Panel</h1>
<p className="text-textMuted">Verwaltung von NPCs und System-Einstellungen</p>
</div>
<button onClick={onBack} className="flex items-center gap-2 text-sm text-textMuted hover:text-textMain transition-colors">
<span className="text-lg"></span> Zurück zur Übersicht
</button>
</div>
{/* Navigation Tabs */}
<div className="flex gap-1 border-b border-border mb-8 overflow-x-auto">
<button
onClick={() => setActiveTab('overview')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'overview' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
Übersicht
</button>
<button
onClick={() => setActiveTab('edit-npcs')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'edit-npcs' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
NPCs verwalten ({npcs.citizens.length + npcs.companies.length})
</button>
<button
onClick={() => setActiveTab('create-npc')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'create-npc' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
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 */}
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 mb-6">
<p className="text-red-400">{error}</p>
</div>
)}
{/* Content */}
<div className="space-y-8">
{activeTab === 'overview' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="bg-surface border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-blue-500/10 rounded text-blue-400">
<Icons.Users className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-white">NPC-Bürger</h3>
<p className="text-xs text-textMuted">Erstellte NPCs</p>
</div>
</div>
<div className="text-3xl font-bold text-white">{npcs.citizens.length}</div>
</div>
<div className="bg-surface border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-purple-500/10 rounded text-purple-400">
<Icons.ShoppingBag className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-white">NPC-Firmen</h3>
<p className="text-xs text-textMuted">Erstellte Unternehmen</p>
</div>
</div>
<div className="text-3xl font-bold text-white">{npcs.companies.length}</div>
</div>
<div className="bg-surface border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-green-500/10 rounded text-green-400">
<Icons.Shield className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-white">System-Status</h3>
<p className="text-xs text-textMuted">Admin-Berechtigung</p>
</div>
</div>
<div className="text-lg font-bold text-green-400">Aktiv</div>
</div>
</div>
)}
{activeTab === 'edit-npcs' && (
<div className="space-y-8">
{/* Edit NPC Citizens */}
<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.Edit className="w-5 h-5 text-blue-400" />
NPC-Bürger bearbeiten
</h3>
{npcs.citizens.length === 0 ? (
<p className="text-textMuted">Keine NPC-Bürger vorhanden.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{npcs.citizens.map((citizen: any) => (
<EditNpcCitizenCard
key={citizen.uuid}
citizen={citizen}
onUpdate={() => loadNpcs()}
onError={setError}
/>
))}
</div>
)}
</div>
{/* Edit NPC Companies */}
<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.Edit className="w-5 h-5 text-purple-400" />
NPC-Firmen bearbeiten
</h3>
{npcs.companies.length === 0 ? (
<p className="text-textMuted">Keine NPC-Firmen vorhanden.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{npcs.companies.map((company: any) => (
<EditNpcCompanyCard
key={company.id}
company={company}
npcCitizens={npcs.citizens}
onUpdate={() => loadNpcs()}
onError={setError}
onOpenShopModal={(projectId, shopItems) =>
setShopModalState({ isOpen: true, projectId, shopItems })
}
/>
))}
</div>
)}
</div>
</div>
)}
{activeTab === 'create-npc' && (
<div className="space-y-8">
{/* Create NPC Citizen */}
<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.Users className="w-5 h-5 text-blue-400" />
NPC-Bürger 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">Name *</label>
<input
type="text"
value={citizenForm.username}
onChange={(e) => setCitizenForm({...citizenForm, username: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. Händler Karl"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Rolle</label>
<select
value={citizenForm.role}
onChange={(e) => setCitizenForm({...citizenForm, role: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="Bürger">Bürger</option>
<option value="Händler">Händler</option>
<option value="Schmied">Schmied</option>
<option value="Bauer">Bauer</option>
<option value="Wächter">Wächter</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Tags (komma-getrennt)</label>
<input
type="text"
value={citizenForm.tags}
onChange={(e) => setCitizenForm({...citizenForm, tags: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. #Bürger, #Händler, #Fleischer"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Stadt-Zugehörigkeit</label>
<select
value={citizenForm.organizationId}
onChange={(e) => setCitizenForm({...citizenForm, organizationId: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="">Keine Stadt</option>
<option value="org-3">Provisorium Null</option>
<option value="org-4">Sakura</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Charakter-Beschreibung</label>
<textarea
value={citizenForm.storyMarkdown}
onChange={(e) => setCitizenForm({...citizenForm, storyMarkdown: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="Kurze Beschreibung des NPCs..."
/>
</div>
<div className="md:col-span-2 pt-2">
<button
onClick={createNpcCitizen}
disabled={loading || !citizenForm.username.trim()}
className="w-full bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-medium disabled:cursor-not-allowed"
>
{loading ? 'Erstelle...' : 'NPC-Bürger erstellen'}
</button>
</div>
</div>
</div>
{/* Create NPC Company */}
<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.ShoppingBag className="w-5 h-5 text-purple-400" />
NPC-Firma 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">Firmenname *</label>
<input
type="text"
value={companyForm.title}
onChange={(e) => setCompanyForm({...companyForm, title: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. Karls Fleischerei"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Kategorie</label>
<select
value={companyForm.category}
onChange={(e) => setCompanyForm({...companyForm, category: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="Enterprise">Enterprise</option>
<option value="Service">Service</option>
<option value="Story Arc">Story Arc</option>
<option value="Black Market">Black Market</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">NPC-Eigentümer *</label>
<select
value={companyForm.owner}
onChange={(e) => setCompanyForm({...companyForm, owner: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="">NPC auswählen...</option>
{npcs.citizens.map((citizen: any) => (
<option key={citizen.uuid} value={citizen.username}>
{citizen.username} ({citizen.stats.role})
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Stadt-Zugehörigkeit</label>
<select
value={companyForm.associatedOrgId}
onChange={(e) => setCompanyForm({...companyForm, associatedOrgId: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="">Keine Stadt</option>
<option value="org-3">Provisorium Null</option>
<option value="org-4">Sakura</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Beschreibung</label>
<textarea
value={companyForm.description}
onChange={(e) => setCompanyForm({...companyForm, description: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="Beschreibung der NPC-Firma..."
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Shop-Artikel (JSON-Format)</label>
<textarea
value={companyForm.shopItems}
onChange={(e) => setCompanyForm({...companyForm, shopItems: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm font-mono text-xs"
placeholder='[{"name": "Steak", "description": "Frisches Fleisch", "price": 5, "currency": "Gold", "stock": 10, "type": "item"}]'
/>
<p className="text-xs text-textMuted mt-1">Optional: JSON-Array mit Shop-Artikeln</p>
</div>
<div className="md:col-span-2 pt-2">
<button
onClick={createNpcCompany}
disabled={loading || !companyForm.title.trim() || !companyForm.owner.trim()}
className="w-full bg-purple-500 hover:bg-purple-600 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-medium disabled:cursor-not-allowed"
>
{loading ? 'Erstelle...' : 'NPC-Firma erstellen'}
</button>
</div>
</div>
</div>
</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) {
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) {
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) {
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}
onClose={() => setShopModalState({ isOpen: false, projectId: '', shopItems: [] })}
shopItems={shopModalState.shopItems}
onUpdate={(updatedItems) => {
setShopModalState(prev => ({ ...prev, shopItems: updatedItems }));
// Trigger a reload to show updated data
loadNpcs();
}}
projectId={shopModalState.projectId}
onError={setError}
/>
</div>
</div>
);
};
export default AdminPage;