Files
project_vollidioten_website/pages/CityProfile.tsx
Lars Behrends 2481187fe7 routing
2025-12-28 17:21:30 +01:00

305 lines
16 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Organization, Project, Player } from '../types';
import { Icons } from '../components/IconSet';
import { dbService } from '../services/DatabaseService';
const CityProfile: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [city, setCity] = useState<Organization | null>(null);
const backLabel = 'Zurück zu Städte';
const onSelectPlayer = (playerId: string) => navigate(`/players/${playerId}`);
const onSelectProject = (projectId: string) => navigate(`/projects/${projectId}`);
const [activeTab, setActiveTab] = useState<'overview' | 'residents' | 'ventures'>('overview');
const [residents, setResidents] = useState<Player[]>([]);
const [ventures, setVentures] = useState<Project[]>([]);
const [cityStats, setCityStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!id) {
navigate('/cities');
return;
}
// Load city data
const cityData = dbService.getOrg(id);
if (cityData) {
setCity(cityData);
} else {
navigate('/cities');
return;
}
setLoading(false);
}, [id, navigate]);
useEffect(() => {
if (!city) return;
const loadCityData = () => {
// Load residents (players in this city)
const allPlayers = dbService.getPlayers();
const cityResidents = allPlayers.filter(p => p.stats.organizationId === city.id);
setResidents(cityResidents);
// Load ventures (projects in this city)
const allProjects = dbService.getProjects();
const cityVentures = allProjects.filter(p => p.associatedOrgId === city.id);
setVentures(cityVentures);
// Calculate dynamic city statistics
const stats = {
citizens: cityResidents.length,
businesses: cityVentures.length,
activeBusinesses: cityVentures.filter(p => p.status === 'active').length,
recruitingBusinesses: cityVentures.filter(p => p.hiring).length,
// Merge with existing static stats
...city.cityStats
};
setCityStats(stats);
};
// Initial load
loadCityData();
// Subscribe to updates
const unsub = dbService.subscribe(loadCityData);
return unsub;
}, [city]);
if (loading || !city) {
return (
<div className="max-w-4xl mx-auto animate-in slide-in-from-right-4 duration-300">
<div className="flex justify-center py-20">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accentInfo"></div>
</div>
</div>
);
}
return (
<div className="animate-in slide-in-from-right-4 duration-300">
<button onClick={() => navigate('/cities')} className="flex items-center gap-2 text-sm text-textMuted hover:text-textMain mb-6 transition-colors group">
<span className="group-hover:-translate-x-1 transition-transform"></span> {backLabel}
</button>
{/* Hero Header */}
<div className="relative h-64 md:h-80 rounded-2xl overflow-hidden border border-border mb-8 shadow-card">
<img
src={city.bannerUrl || 'https://images.unsplash.com/photo-1562774053-701939374585?q=80&w=2086&auto=format&fit=crop'}
alt={city.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/50 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-8">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
{city.establishedYear && (
<div className="text-accentInfo font-mono text-xs mb-2 bg-black/40 inline-block px-2 py-1 rounded backdrop-blur border border-white/5">
GEGR. {city.establishedYear}
</div>
)}
<h1 className="text-4xl md:text-6xl font-bold text-white tracking-tight mb-2">{city.name}</h1>
<div className="flex items-center gap-4 text-sm text-gray-300">
<span className="flex items-center gap-1.5">
<Icons.Users className="w-4 h-4" /> {city.memberCount} Mitglieder
</span>
<span className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-accentSuccess animate-pulse" /> {city.status}
</span>
</div>
</div>
{city.mayor && (
<div className="text-right">
<div className="text-xs text-textMuted uppercase tracking-widest mb-1">Aktueller Anführer</div>
<div className="flex items-center gap-2 text-lg font-medium text-amber-100 bg-surfaceHighlight/60 px-4 py-2 rounded-lg backdrop-blur border border-amber-500/30 shadow-[0_0_15px_rgba(245,158,11,0.15)] group hover:border-amber-500/50 transition-colors">
<Icons.Crown className="w-5 h-5 text-amber-400 drop-shadow-md" />
{city.mayor}
</div>
</div>
)}
</div>
</div>
</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 & Galerie
</button>
<button
onClick={() => setActiveTab('residents')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'residents' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
Bewohner ({residents.length})
</button>
<button
onClick={() => setActiveTab('ventures')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${activeTab === 'ventures' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
>
Lokale Unternehmen ({ventures.length})
</button>
</div>
{/* Content Area */}
<div className="min-h-[400px]">
{activeTab === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-8">
<section>
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
<Icons.Map className="w-5 h-5 text-accentInfo" /> Über {city.name}
</h3>
<p className="text-textMuted leading-relaxed text-lg">
{city.description}
</p>
</section>
{city.gallery && city.gallery.length > 0 && (
<section>
<h3 className="text-xl font-bold mb-4">Bildarchiv</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{city.gallery.map((url, idx) => (
<div key={idx} className="rounded-xl overflow-hidden aspect-video border border-border group relative">
<img src={url} alt={`Gallery ${idx}`} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
</div>
))}
</div>
</section>
)}
</div>
{city.cityStats && (
<div className="bg-surface border border-border rounded-xl p-6 h-fit">
<h4 className="text-sm font-bold uppercase text-textMuted mb-6 flex items-center gap-2">
<Icons.Terminal className="w-4 h-4" /> Stadt-Statistiken
</h4>
<div className="space-y-6">
{/* Tax Rate */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-500/10 rounded text-green-400">
<Icons.Coins className="w-4 h-4" />
</div>
<span className="text-sm text-gray-400">Steuersatz</span>
</div>
<div className="text-right">
<span className="font-mono text-lg text-white font-medium">{city.cityStats.taxRate}%</span>
<div className="text-[10px] text-textMuted">Pro Transaktion</div>
</div>
</div>
<div className="w-full bg-white/5 h-px" />
{/* Defense */}
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/10 rounded text-blue-400">
<Icons.Shield className="w-4 h-4" />
</div>
<span className="text-sm text-gray-400">Verteidigungswert</span>
</div>
<span className="font-mono text-white font-medium">{city.cityStats.defenseRating}/10</span>
</div>
<div className="h-1.5 w-full bg-surfaceHighlight rounded-full overflow-hidden">
<div className="h-full bg-blue-500/70" style={{ width: `${(city.cityStats.defenseRating / 10) * 100}%` }} />
</div>
</div>
<div className="w-full bg-white/5 h-px" />
{/* Gov Type */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-500/10 rounded text-purple-400">
<Icons.Scroll className="w-4 h-4" />
</div>
<span className="text-sm text-gray-400">Regierungsform</span>
</div>
<span className="text-sm text-white font-medium">{city.cityStats.government}</span>
</div>
<div className="w-full bg-white/5 h-px" />
{/* Biome */}
<div className="flex justify-between items-center py-1">
<span className="text-sm text-gray-400">Biom</span>
<span className="text-sm text-white">{city.cityStats.biome}</span>
</div>
<div className="flex justify-between items-center py-1">
<span className="text-sm text-gray-400">Spezialität</span>
<span className="text-sm text-white">{city.cityStats.specialty}</span>
</div>
</div>
</div>
)}
</div>
)}
{activeTab === 'residents' && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{residents.map(player => (
<div
key={player.uuid}
onClick={() => onSelectPlayer(player.uuid)}
className="bg-surface border border-border p-4 rounded-xl flex items-center gap-4 hover:border-accentInfo/50 transition-colors cursor-pointer group hover:shadow-card hover:bg-surfaceHighlight/30"
>
<div className="w-10 h-10 bg-surfaceHighlight rounded flex items-center justify-center font-bold text-lg text-textMuted group-hover:text-textMain transition-colors">
{player.username.charAt(0)}
</div>
<div>
<div className="font-medium text-white group-hover:text-accentInfo transition-colors">{player.username}</div>
<div className="text-xs text-textMuted">{player.stats.role}</div>
</div>
</div>
))}
{residents.length === 0 && <div className="text-textMuted italic">Keine Bewohner öffentlich registriert.</div>}
</div>
)}
{activeTab === 'ventures' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{ventures.map(project => (
<div
key={project.id}
onClick={() => onSelectProject(project.id)}
className="bg-surface border border-border rounded-xl p-5 hover:border-accentInfo/50 transition-all cursor-pointer group hover:shadow-card hover:bg-surfaceHighlight/30"
>
<div className="flex justify-between items-start mb-2">
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded ${
project.category === 'Story Arc' ? 'bg-orange-500/10 text-orange-400' : 'bg-blue-500/10 text-blue-400'
}`}>
{project.category}
</span>
{project.hiring && (
<span className="text-[10px] font-bold text-accentSuccess animate-pulse">STELLEN</span>
)}
</div>
<h3 className="text-lg font-bold text-white mb-1 group-hover:text-accentInfo transition-colors">{project.title}</h3>
<p className="text-sm text-textMuted mb-4 line-clamp-2">{project.description}</p>
<div className="flex items-center justify-between pt-4 border-t border-white/5">
<span className="text-xs text-textMuted">Geführt von {project.owner}</span>
<div className="text-xs font-mono text-white">{project.progress}% Fortschr.</div>
</div>
</div>
))}
{ventures.length === 0 && <div className="text-textMuted italic">Keine aktiven Unternehmen in dieser Organisation.</div>}
</div>
)}
</div>
</div>
);
};
export default CityProfile;