mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
Refactor CityProfile and PlayerProfile components for improved data fetching and error handling; add NPC management modals for banner, gallery, and logo with enhanced user experience and error feedback.
This commit is contained in:
139
pages/Admin.tsx
139
pages/Admin.tsx
@@ -1,6 +1,9 @@
|
||||
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;
|
||||
@@ -453,6 +456,9 @@ const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate:
|
||||
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 || '',
|
||||
@@ -606,42 +612,89 @@ const EditNpcCompanyCard: React.FC<{ company: any; npcCitizens: any[]; onUpdate:
|
||||
}
|
||||
|
||||
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 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>
|
||||
<h4 className="font-medium text-white">{company.title}</h4>
|
||||
<p className="text-xs text-textMuted">{company.category}</p>
|
||||
<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="flex gap-2">
|
||||
<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 className="text-xs text-textMuted">
|
||||
Eigentümer: {company.owner}
|
||||
</div>
|
||||
<div className="text-xs text-textMuted">
|
||||
Eigentümer: {company.owner}
|
||||
{company.shopCatalog && company.shopCatalog.length > 0 && (
|
||||
<div className="text-xs text-accentInfo mt-1">
|
||||
Shop: {company.shopCatalog.length} Artikel
|
||||
</div>
|
||||
)}
|
||||
</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()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -666,13 +719,24 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
|
||||
return unsub;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'npcs' && isAdmin) {
|
||||
useEffect(() => {
|
||||
loadNpcs();
|
||||
}
|
||||
if (activeTab === 'cities' && isAdmin) {
|
||||
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 () => {
|
||||
@@ -1677,7 +1741,6 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Banner erfolgreich aktualisiert!');
|
||||
loadCities(); // Reload to show updated image
|
||||
} else {
|
||||
setError('Fehler beim Hochladen des Banners');
|
||||
@@ -1727,7 +1790,6 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Logo erfolgreich aktualisiert!');
|
||||
loadCities(); // Reload to show updated image
|
||||
} else {
|
||||
setError('Fehler beim Hochladen des Logos');
|
||||
@@ -1781,7 +1843,6 @@ const AdminPage: React.FC<AdminPageProps> = ({ onBack }) => {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Stadt erfolgreich aktualisiert!');
|
||||
setEditingCity(null);
|
||||
loadCities();
|
||||
} else {
|
||||
|
||||
@@ -24,16 +24,32 @@ const CityProfile: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load city data
|
||||
const cityData = dbService.getOrg(id);
|
||||
if (cityData) {
|
||||
setCity(cityData);
|
||||
} else {
|
||||
navigate('/cities');
|
||||
return;
|
||||
}
|
||||
// Fetch fresh data from database on mount/reload
|
||||
const loadCityData = async () => {
|
||||
try {
|
||||
await dbService.fetchAll();
|
||||
const cityData = dbService.getOrg(id);
|
||||
if (cityData) {
|
||||
setCity(cityData);
|
||||
} else {
|
||||
navigate('/cities');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch fresh city data:', error);
|
||||
// Fallback to cached data
|
||||
const cityData = dbService.getOrg(id);
|
||||
if (cityData) {
|
||||
setCity(cityData);
|
||||
} else {
|
||||
navigate('/cities');
|
||||
return;
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
setLoading(false);
|
||||
loadCityData();
|
||||
}, [id, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -42,7 +58,7 @@ const CityProfile: React.FC = () => {
|
||||
const loadCityData = () => {
|
||||
// Load residents (players in this city)
|
||||
const allPlayers = dbService.getPlayers();
|
||||
const cityResidents = allPlayers.filter(p => p.stats.organizationId === city.id);
|
||||
const cityResidents = allPlayers.filter(p => p.organizationId === city.id);
|
||||
setResidents(cityResidents);
|
||||
|
||||
// Load ventures (projects in this city)
|
||||
|
||||
@@ -21,6 +21,19 @@ const PlayerProfile: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'story' | 'stats' | 'projects'>('story');
|
||||
|
||||
const handleNavigateToOrg = (orgId: string) => {
|
||||
const org = dbService.getOrg(orgId);
|
||||
if (org?.type === 'City') {
|
||||
navigate(`/cities/${orgId}`);
|
||||
} else {
|
||||
navigate(`/organizations/${orgId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigateToProject = (projectId: string) => {
|
||||
navigate(`/projects/${projectId}`);
|
||||
};
|
||||
|
||||
// Is this the logged-in user's profile?
|
||||
const isOwner = currentUser?.linkedPlayerUuid === player?.uuid;
|
||||
const playerOrg = player ? dbService.getOrg(player.organizationId || '') : null;
|
||||
@@ -36,17 +49,37 @@ const PlayerProfile: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load player data
|
||||
const playerData = dbService.getPlayer(id);
|
||||
if (playerData) {
|
||||
setPlayer(playerData);
|
||||
} else {
|
||||
navigate('/players');
|
||||
return;
|
||||
}
|
||||
// Load player data - try to fetch from API first, fallback to cache
|
||||
const loadPlayerData = async () => {
|
||||
try {
|
||||
const playerData = await dbService.fetchPlayer(id);
|
||||
if (playerData) {
|
||||
setPlayer(playerData);
|
||||
} else {
|
||||
// Fallback to cache if API fails
|
||||
const cachedPlayer = dbService.getPlayer(id);
|
||||
if (cachedPlayer) {
|
||||
setPlayer(cachedPlayer);
|
||||
} else {
|
||||
navigate('/players');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading player data:', error);
|
||||
// Fallback to cache
|
||||
const cachedPlayer = dbService.getPlayer(id);
|
||||
if (cachedPlayer) {
|
||||
setPlayer(cachedPlayer);
|
||||
} else {
|
||||
navigate('/players');
|
||||
return;
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
console.log(playerData);
|
||||
setLoading(false);
|
||||
loadPlayerData();
|
||||
}, [id, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -252,27 +285,38 @@ const PlayerProfile: React.FC = () => {
|
||||
|
||||
<div className="bg-surface border border-border rounded-xl p-4 shadow-card">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-textMuted mb-3">Zugehörigkeit</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded flex items-center justify-center text-lg font-bold border border-white/5 ${
|
||||
playerOrg
|
||||
? (playerOrg.type === 'City' ? 'bg-blue-500/10 text-blue-400' :
|
||||
playerOrg.type === 'Guild' ? 'bg-amber-500/10 text-amber-400' :
|
||||
'bg-purple-500/10 text-purple-400')
|
||||
: 'bg-surfaceHighlight text-textMuted'
|
||||
}`}>
|
||||
{playerOrg ? playerOrg.name.charAt(0) : <Icons.Map className="w-5 h-5 opacity-50" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-textMain">{player.minecraftStats?.role || 'Unbekannt'}</div>
|
||||
<div className="text-xs text-textMuted">
|
||||
{playerOrg ? (
|
||||
<span className="group-hover:text-accentInfo transition-colors">{playerOrg.name}</span>
|
||||
) : (
|
||||
'Freiberufler / Keine Zugehörigkeit'
|
||||
)}
|
||||
{playerOrg ? (
|
||||
<div
|
||||
onClick={() => handleNavigateToOrg(playerOrg.id)}
|
||||
className="flex items-center gap-3 cursor-pointer hover:bg-surfaceHighlight/50 -m-1 p-1 rounded transition-colors group"
|
||||
>
|
||||
<div className={`w-10 h-10 rounded flex items-center justify-center text-lg font-bold border border-white/5 ${
|
||||
playerOrg.type === 'City' ? 'bg-blue-500/10 text-blue-400' :
|
||||
playerOrg.type === 'Guild' ? 'bg-amber-500/10 text-amber-400' :
|
||||
'bg-purple-500/10 text-purple-400'
|
||||
}`}>
|
||||
{playerOrg.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-textMain group-hover:text-accentInfo transition-colors">
|
||||
{playerOrg.name}
|
||||
</div>
|
||||
<div className="text-xs text-textMuted">
|
||||
{player.minecraftStats?.role || 'Unbekannt'} • Klick zum Anzeigen
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded flex items-center justify-center text-lg font-bold border border-white/5 bg-surfaceHighlight text-textMuted">
|
||||
<Icons.Map className="w-5 h-5 opacity-50" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-textMain">Freiberufler</div>
|
||||
<div className="text-xs text-textMuted">Keine Zugehörigkeit</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -437,9 +481,15 @@ const PlayerProfile: React.FC = () => {
|
||||
<div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{ownedProjects.map((project) => (
|
||||
<div key={project.id} className="bg-surfaceHighlight/30 border border-border rounded-lg p-4 hover:border-accentInfo/50 transition-all">
|
||||
<div
|
||||
key={project.id}
|
||||
onClick={() => handleNavigateToProject(project.id)}
|
||||
className="bg-surfaceHighlight/30 border border-border rounded-lg p-4 hover:border-accentInfo/50 transition-all cursor-pointer group"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h4 className="font-medium text-textMain">{project.title}</h4>
|
||||
<h4 className="font-medium text-textMain group-hover:text-accentInfo transition-colors">
|
||||
{project.title}
|
||||
</h4>
|
||||
<span className={`text-xs px-2 py-1 rounded border ${
|
||||
project.status === 'active' ? 'bg-green-500/10 text-green-400 border-green-500/20' :
|
||||
project.status === 'recruiting' ? 'bg-blue-500/10 text-blue-400 border-blue-500/20' :
|
||||
@@ -468,6 +518,9 @@ const PlayerProfile: React.FC = () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-accentInfo mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Klick zum Anzeigen →
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user