mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
feat: implement Players component with data fetching and loading state; remove mock data and enhance error handling in database service
This commit is contained in:
38
App.tsx
38
App.tsx
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import Layout from './components/Layout';
|
import Layout from './components/Layout';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
|
import Players from './pages/Players';
|
||||||
import PlayerProfile from './pages/PlayerProfile';
|
import PlayerProfile from './pages/PlayerProfile';
|
||||||
import SetupGuide from './pages/SetupGuide';
|
import SetupGuide from './pages/SetupGuide';
|
||||||
import Projects from './pages/Projects';
|
import Projects from './pages/Projects';
|
||||||
@@ -109,42 +110,7 @@ function App() {
|
|||||||
<Route path="/datapack" element={<DatapackGenerator />} />
|
<Route path="/datapack" element={<DatapackGenerator />} />
|
||||||
|
|
||||||
{/* Player Routes */}
|
{/* Player Routes */}
|
||||||
<Route path="/players" element={
|
<Route path="/players" element={<Players onSelectPlayer={navigateToPlayer} />} />
|
||||||
<div className="animate-in fade-in">
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h2 className="text-2xl font-bold">Bürgerverzeichnis</h2>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Filtern nach Tag oder Name..."
|
|
||||||
className="bg-surfaceHighlight border border-border rounded-lg pl-10 pr-4 py-2 text-sm text-textMain focus:border-accentInfo focus:outline-none w-64 transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
||||||
{players.map(player => (
|
|
||||||
<div
|
|
||||||
key={player.uuid}
|
|
||||||
onClick={() => navigateToPlayer(player.uuid)}
|
|
||||||
className="group bg-surface border border-border p-4 rounded-xl cursor-pointer hover:border-accentInfo/50 transition-all duration-200 hover:shadow-card"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-12 h-12 rounded-md flex items-center justify-center font-bold text-lg text-textMuted group-hover:text-textMain transition-colors">
|
|
||||||
<img src={"https://minotar.net/armor/bust/"+player.username+"/500.png"}></img>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold text-textMain group-hover:text-accentInfo transition-colors">{player.username}</div>
|
|
||||||
<div className="text-xs text-textMuted mt-1 flex gap-2">
|
|
||||||
{player.tags.slice(0, 2).map(t => <span key={t}>{t}</span>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} />
|
|
||||||
<Route path="/players/:id" element={<PlayerProfile />} />
|
<Route path="/players/:id" element={<PlayerProfile />} />
|
||||||
|
|
||||||
{/* City Routes */}
|
{/* City Routes */}
|
||||||
|
|||||||
191
constants.ts
191
constants.ts
@@ -1,191 +0,0 @@
|
|||||||
import { Player, Organization, Project } from './types';
|
|
||||||
|
|
||||||
export const MOCK_PLAYERS: Player[] = [
|
|
||||||
{
|
|
||||||
uuid: '80301bff-74df-4579-bcfc-082ac8d26b5b',
|
|
||||||
username: 'kaiwastoshort',
|
|
||||||
isOnline: true,
|
|
||||||
tags: ['#Bürger', '#Händler'],
|
|
||||||
stats: {
|
|
||||||
playtimeHours: 482,
|
|
||||||
level: 45,
|
|
||||||
role: 'Bürger',
|
|
||||||
organizationId: 'org-3' // Moved to Old Haven
|
|
||||||
},
|
|
||||||
inventory: [
|
|
||||||
{ id: 'minecraft:diamond_pickaxe', name: 'Behutsamkeit Hacke', count: 1, type: 'tool', rarity: 'epic', nbtSummary: 'Eff V, Rep.' },
|
|
||||||
{ id: 'minecraft:stone_bricks', name: 'Polierter Andesit', count: 64, type: 'block' },
|
|
||||||
{ id: 'minecraft:stone_bricks', name: 'Polierter Andesit', count: 32, type: 'block' },
|
|
||||||
null, null,
|
|
||||||
{ id: 'minecraft:cooked_beef', name: 'Steak', count: 12, type: 'consumable' },
|
|
||||||
{ id: 'minecraft:compass', name: 'Stadtkarte', count: 1, type: 'misc', nbtSummary: 'Ziel: Spawn' },
|
|
||||||
null, null
|
|
||||||
],
|
|
||||||
storyMarkdown: `
|
|
||||||
# Der Bauplan von V
|
|
||||||
|
|
||||||
> "Stein erinnert sich an das, was Eisen vergisst."
|
|
||||||
|
|
||||||
**V** kam während der *Großen Expansion* ins Tal. Ursprünglich ein einfacher Maurer, stieg er nach dem Entwurf des **Aquädukt-Systems** schnell in den Rängen auf.
|
|
||||||
|
|
||||||
### Bekannte Verbindungen
|
|
||||||
* Die Händlergilde
|
|
||||||
* Nördliche Entdecker
|
|
||||||
|
|
||||||
### Aktuelles Ziel
|
|
||||||
Restaurierung des verfallenen Wachturms am östlichen Grat.
|
|
||||||
`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: '8984c0b5-d912-4462-b189-c864fba4a1af',
|
|
||||||
username: 'DrKButz',
|
|
||||||
isOnline: false,
|
|
||||||
tags: ['#Bauunternehmer'],
|
|
||||||
stats: {
|
|
||||||
playtimeHours: 120,
|
|
||||||
level: 12,
|
|
||||||
role: 'Unternehmer',
|
|
||||||
organizationId: 'org-4'
|
|
||||||
},
|
|
||||||
inventory: [
|
|
||||||
{ id: 'minecraft:redstone', name: 'Redstone Staub', count: 64, type: 'misc' },
|
|
||||||
{ id: 'minecraft:comparator', name: 'Komparator', count: 16, type: 'misc' },
|
|
||||||
null,
|
|
||||||
{ id: 'minecraft:book', name: 'Forschungsnotizen', count: 1, type: 'misc', nbtSummary: 'Seite 12: Logikgatter' },
|
|
||||||
null, null, null, null, null
|
|
||||||
],
|
|
||||||
storyMarkdown: `
|
|
||||||
# Forschungslogbuch:
|
|
||||||
|
|
||||||
Spezialisiert auf automatisierte Logistik. Gerüchten zufolge hat er eine Maschine gebaut, die Items schneller sortieren kann als jeder Mensch.
|
|
||||||
|
|
||||||
* Sucht aktuell: Quarz
|
|
||||||
* Verkauft: Auto-Schmelzöfen
|
|
||||||
`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'b3b84518-03a2-4b48-8551-448c3f7a7d77',
|
|
||||||
username: 'ceratic_test',
|
|
||||||
isOnline: true,
|
|
||||||
tags: ['#Bürger', '#Händler'],
|
|
||||||
stats: {
|
|
||||||
playtimeHours: 50,
|
|
||||||
level: 5,
|
|
||||||
role: 'Bürger',
|
|
||||||
organizationId: 'org-3'
|
|
||||||
},
|
|
||||||
inventory: [],
|
|
||||||
storyMarkdown: 'Einfach nur eine einfache Bäckerin, die gutes Brot liebt.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: '3c10008b-1eb2-4d89-8b8a-6d03c821eb09',
|
|
||||||
username: 'merkursun',
|
|
||||||
isOnline: true,
|
|
||||||
tags: ['#Bürger'],
|
|
||||||
stats: {
|
|
||||||
playtimeHours: 50,
|
|
||||||
level: 5,
|
|
||||||
role: 'Bürger',
|
|
||||||
organizationId: 'org-4'
|
|
||||||
},
|
|
||||||
inventory: [],
|
|
||||||
storyMarkdown: 'Einfach nur eine einfache Bäckerin, die gutes Brot liebt.'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MOCK_ORGS: Organization[] = [
|
|
||||||
/* {
|
|
||||||
id: 'org-2',
|
|
||||||
name: 'Butz Building GmbH',
|
|
||||||
type: 'Company',
|
|
||||||
description: 'Baugewerbe.',
|
|
||||||
memberCount: 1,
|
|
||||||
status: 'active'
|
|
||||||
},*/
|
|
||||||
{
|
|
||||||
id: 'org-3',
|
|
||||||
name: 'Provisorium Null',
|
|
||||||
type: 'City',
|
|
||||||
description: 'Die erste Siedlung, eingebettet zwischen den Zwillingsgipfeln. Bekannt für das geschäftige Marktviertel und die alten Steinmauern.',
|
|
||||||
memberCount: 6,
|
|
||||||
status: 'active',
|
|
||||||
mayor: '',
|
|
||||||
establishedYear: 'Day 0',
|
|
||||||
bannerUrl: 'images/screenshots/2025-12-28_01.11.10.png',
|
|
||||||
gallery: [
|
|
||||||
'images/screenshots/2025-12-28_01.12.07.png', // Market
|
|
||||||
'images/screenshots/2025-12-28_01.12.17.png', // Castle
|
|
||||||
'images/screenshots/2025-12-28_01.12.24.png' // Walls
|
|
||||||
],
|
|
||||||
cityStats: {
|
|
||||||
taxRate: 3.5,
|
|
||||||
biome: 'Ebene / Gebirge',
|
|
||||||
defenseRating: 8,
|
|
||||||
government: 'Feudalmonarchie',
|
|
||||||
specialty: 'Handel & Stein'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'org-4',
|
|
||||||
name: 'Sakura',
|
|
||||||
type: 'City',
|
|
||||||
description: 'Eine dunkle, biolumineszente Hafenstadt in den tiefen Höhlen. Heimat von Schmugglern und Händlern seltener Mineralien.',
|
|
||||||
memberCount: 2,
|
|
||||||
status: 'active',
|
|
||||||
mayor: 'Kampfzwerk',
|
|
||||||
establishedYear: 'Ära 2, Jahr 10',
|
|
||||||
bannerUrl: 'images/screenshots/2025-12-28_01.11.32.png',
|
|
||||||
gallery: [
|
|
||||||
'images/screenshots/2025-12-28_01.11.38.png',
|
|
||||||
'images/screenshots/2025-12-28_01.11.44.png'
|
|
||||||
],
|
|
||||||
cityStats: {
|
|
||||||
taxRate: 15.0,
|
|
||||||
biome: 'Deep Dark / Lush Caves',
|
|
||||||
defenseRating: 4,
|
|
||||||
government: 'Syndikat',
|
|
||||||
specialty: 'Schmuggelware & Erze'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MOCK_PROJECTS: Project[] = [
|
|
||||||
{
|
|
||||||
id: 'ven-1',
|
|
||||||
title: 'DrkButz Architektur & Mauerwerk',
|
|
||||||
description: 'Führendes Architekturbüro, spezialisiert auf gotische Strukturen und Verteidigungsmauern. Wir entwerfen dein Vermächtnis.',
|
|
||||||
category: 'Enterprise',
|
|
||||||
status: 'active',
|
|
||||||
progress: 85, // Reputation
|
|
||||||
owner: 'DrKButz',
|
|
||||||
employees: [],
|
|
||||||
hiring: false,
|
|
||||||
foundedDate: 'Zyklus 12',
|
|
||||||
associatedOrgId: 'org-4', // Old Haven
|
|
||||||
bannerUrl: 'images/screenshots/2025-12-28_01.11.49.png',
|
|
||||||
shopCatalog: [
|
|
||||||
{ id: 's2', name: 'Geländevermessung', description: '1 Stunde Beratung zur Geländevorbereitung.', price: 20, currency: 'Gold', stock: 10, type: 'service' },
|
|
||||||
{ id: 's3', name: 'Mauerbau', description: 'Wir bauen deine Außenmauern. Preis pro Chunk.', price: 100, currency: 'Gold', stock: 5, type: 'service', materialsRequired: 'Kunde stellt Steinziegel' },
|
|
||||||
{ id: 's4', name: 'Terraforming', description: 'Einebnung und Landschaftsbau.', price: 75, currency: 'Gold', stock: 2, type: 'service' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ven-5',
|
|
||||||
title: 'Tinas Tavern',
|
|
||||||
description: 'Täglich frisches Brot zum Spawn-Markt geliefert. Lieferant der königlichen Garde.',
|
|
||||||
category: 'Enterprise',
|
|
||||||
status: 'active',
|
|
||||||
progress: 98,
|
|
||||||
owner: 'kaiwastoshort',
|
|
||||||
employees: [],
|
|
||||||
hiring: false,
|
|
||||||
associatedOrgId: 'org-3',
|
|
||||||
bannerUrl: 'images/screenshots/tinas.png',
|
|
||||||
shopCatalog: [
|
|
||||||
{ id: 'f1', name: 'Redstone Block', description: '', price: 2, currency: 'Diamonds', stock: 45, type: 'item' },
|
|
||||||
{ id: 'f2', name: 'Honey', description: '', price: 5, currency: 'Diamonds', stock: 12, type: 'item' },
|
|
||||||
{ id: 'f3', name: 'Slime', description: '', price: 10, currency: 'Diamonds', stock: 4, type: 'item' },
|
|
||||||
{ id: 'f4', name: 'Bier', description: '', price: 3, currency: 'Diamonds', stock: 20, type: 'item' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -53,6 +53,17 @@ const Cities: React.FC<CitiesProps> = ({ onSelectCity }) => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await dbService.fetchAll();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch fresh data:', error);
|
||||||
|
// Continue with cached data
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const loadCities = () => {
|
const loadCities = () => {
|
||||||
// Get all organizations from database
|
// Get all organizations from database
|
||||||
const allOrgs = dbService.getOrgs();
|
const allOrgs = dbService.getOrgs();
|
||||||
@@ -63,7 +74,7 @@ const Cities: React.FC<CitiesProps> = ({ onSelectCity }) => {
|
|||||||
// Count citizens (players with this organizationId)
|
// Count citizens (players with this organizationId)
|
||||||
const allPlayers = dbService.getPlayers();
|
const allPlayers = dbService.getPlayers();
|
||||||
const citizenCount = allPlayers.filter(player =>
|
const citizenCount = allPlayers.filter(player =>
|
||||||
player.stats.organizationId === city.id
|
player.organizationId === city.id
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
// Count businesses/projects in this city
|
// Count businesses/projects in this city
|
||||||
@@ -87,11 +98,10 @@ const Cities: React.FC<CitiesProps> = ({ onSelectCity }) => {
|
|||||||
|
|
||||||
setCities(cityOrgs);
|
setCities(cityOrgs);
|
||||||
setCitiesWithStats(citiesStats);
|
setCitiesWithStats(citiesStats);
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial load
|
// Load fresh data on mount
|
||||||
loadCities();
|
loadData();
|
||||||
|
|
||||||
// Subscribe to updates
|
// Subscribe to updates
|
||||||
const unsub = dbService.subscribe(loadCities);
|
const unsub = dbService.subscribe(loadCities);
|
||||||
|
|||||||
@@ -270,8 +270,8 @@ const CityProfile: React.FC = () => {
|
|||||||
onClick={() => onSelectPlayer(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"
|
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">
|
<div className="w-10 h-10 flex items-center justify-center font-bold text-lg text-textMuted group-hover:text-textMain transition-colors">
|
||||||
{player.username.charAt(0)}
|
<img src={"https://minotar.net/armor/bust/"+player.username+"/500.png"}></img>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-white group-hover:text-accentInfo transition-colors">{player.username}</div>
|
<div className="font-medium text-white group-hover:text-accentInfo transition-colors">{player.username}</div>
|
||||||
|
|||||||
@@ -48,19 +48,26 @@ const Dashboard: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await dbService.fetchAll();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch fresh data:', error);
|
||||||
|
// Continue with cached data
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
// Subscribe to database updates
|
// Subscribe to database updates
|
||||||
const unsubscribe = dbService.subscribe(() => {
|
const unsubscribe = dbService.subscribe(() => {
|
||||||
setPlayers(dbService.getPlayers());
|
setPlayers(dbService.getPlayers());
|
||||||
setProjects(dbService.getProjects());
|
setProjects(dbService.getProjects());
|
||||||
setOrgs(dbService.getOrgs());
|
setOrgs(dbService.getOrgs());
|
||||||
setLoading(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial data load
|
// Load fresh data on mount
|
||||||
setPlayers(dbService.getPlayers());
|
loadData();
|
||||||
setProjects(dbService.getProjects());
|
|
||||||
setOrgs(dbService.getOrgs());
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { MOCK_ORGS } from '../constants';
|
|
||||||
import { Organization } from '../types';
|
import { Organization } from '../types';
|
||||||
import { Icons } from '../components/IconSet';
|
import { Icons } from '../components/IconSet';
|
||||||
|
import { dbService } from '../services/DatabaseService';
|
||||||
|
|
||||||
const OrgCard = ({ org, onClick }: { org: Organization; onClick: () => void }) => (
|
const OrgCard = ({ org, onClick }: { org: Organization; onClick: () => void }) => (
|
||||||
<div
|
<div
|
||||||
@@ -43,8 +43,35 @@ const OrgCard = ({ org, onClick }: { org: Organization; onClick: () => void }) =
|
|||||||
|
|
||||||
const Organizations: React.FC<{ onSelectOrg: (id: string) => void }> = ({ onSelectOrg }) => {
|
const Organizations: React.FC<{ onSelectOrg: (id: string) => void }> = ({ onSelectOrg }) => {
|
||||||
const [filter, setFilter] = useState<'all' | Organization['type']>('all');
|
const [filter, setFilter] = useState<'all' | Organization['type']>('all');
|
||||||
|
const [orgs, setOrgs] = useState<Organization[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const filteredOrgs = MOCK_ORGS.filter(org =>
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await dbService.fetchAll();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch fresh data:', error);
|
||||||
|
// Continue with cached data
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadOrgs = () => {
|
||||||
|
const allOrgs = dbService.getOrgs();
|
||||||
|
setOrgs(allOrgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load fresh data on mount
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
// Subscribe to updates
|
||||||
|
const unsub = dbService.subscribe(loadOrgs);
|
||||||
|
return unsub;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredOrgs = orgs.filter(org =>
|
||||||
filter === 'all' ? true : org.type === filter
|
filter === 'all' ? true : org.type === filter
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -83,9 +110,17 @@ const Organizations: React.FC<{ onSelectOrg: (id: string) => void }> = ({ onSele
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<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 className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
{filteredOrgs.map(org => (
|
{filteredOrgs.map(org => (
|
||||||
<OrgCard key={org.id} org={org} onClick={() => onSelectOrg(org.id)} />
|
<div key={org.id}>
|
||||||
|
<OrgCard org={org} onClick={() => onSelectOrg(org.id)} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -94,6 +129,8 @@ const Organizations: React.FC<{ onSelectOrg: (id: string) => void }> = ({ onSele
|
|||||||
<p>Keine Organisationen gefunden.</p>
|
<p>Keine Organisationen gefunden.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
80
pages/Players.tsx
Normal file
80
pages/Players.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Player } from '../types';
|
||||||
|
import { dbService } from '../services/DatabaseService';
|
||||||
|
|
||||||
|
interface PlayersProps {
|
||||||
|
onSelectPlayer: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Players: React.FC<PlayersProps> = ({ onSelectPlayer }) => {
|
||||||
|
const [players, setPlayers] = useState<Player[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await dbService.fetchAll();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch fresh data:', error);
|
||||||
|
// Continue with cached data
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to database updates
|
||||||
|
const unsubDb = dbService.subscribe(() => {
|
||||||
|
setPlayers(dbService.getPlayers());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load fresh data on mount
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
return unsubDb;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="animate-in fade-in">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold">Bürgerverzeichnis</h2>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Filtern nach Tag oder Name..."
|
||||||
|
className="bg-surfaceHighlight border border-border rounded-lg pl-10 pr-4 py-2 text-sm text-textMain focus:border-accentInfo focus:outline-none w-64 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<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 className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
{players.map(player => (
|
||||||
|
<div
|
||||||
|
key={player.uuid}
|
||||||
|
onClick={() => onSelectPlayer(player.uuid)}
|
||||||
|
className="group bg-surface border border-border p-4 rounded-xl cursor-pointer hover:border-accentInfo/50 transition-all duration-200 hover:shadow-card"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-md flex items-center justify-center font-bold text-lg text-textMuted group-hover:text-textMain transition-colors">
|
||||||
|
<img src={"https://minotar.net/armor/bust/"+player.username+"/500.png"}></img>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-textMain group-hover:text-accentInfo transition-colors">{player.username}</div>
|
||||||
|
<div className="text-xs text-textMuted mt-1 flex gap-2">
|
||||||
|
{player.tags.slice(0, 2).map(t => <span key={t}>{t}</span>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Players;
|
||||||
@@ -138,14 +138,24 @@ const Projects: React.FC<ProjectsProps> = ({ onSelectProject }) => {
|
|||||||
|
|
||||||
// Subscribe to auth and data updates
|
// Subscribe to auth and data updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await dbService.fetchAll();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch fresh data:', error);
|
||||||
|
// Continue with cached data
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const unsubAuth = authService.subscribe(setUser);
|
const unsubAuth = authService.subscribe(setUser);
|
||||||
const unsubDb = dbService.subscribe(() => {
|
const unsubDb = dbService.subscribe(() => {
|
||||||
setProjects(dbService.getProjects());
|
setProjects(dbService.getProjects());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial data load
|
// Load fresh data on mount
|
||||||
setProjects(dbService.getProjects());
|
loadData();
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubAuth();
|
unsubAuth();
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Mock Organization",
|
|
||||||
"description": "This is a mock organization that appears when the backend is unavailable.",
|
|
||||||
"leader": "DrKButz",
|
|
||||||
"members": ["8984c0b5-d912-4462-b189-c864fba4a1af"],
|
|
||||||
"gallery": [],
|
|
||||||
"cityStats": {
|
|
||||||
"influence": 50,
|
|
||||||
"reputation": 75
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"uuid": "8984c0b5-d912-4462-b189-c864fba4a1af",
|
|
||||||
"name": "DrKButz",
|
|
||||||
"discordId": "mock_discord_id",
|
|
||||||
"storyMarkdown": "# Mock Player Story\n\nThis is a mock player story that appears when the backend is unavailable.",
|
|
||||||
"tags": ["admin", "developer"],
|
|
||||||
"stats": {
|
|
||||||
"health": 100,
|
|
||||||
"armor": 50,
|
|
||||||
"money": 10000
|
|
||||||
},
|
|
||||||
"inventory": [
|
|
||||||
{"id": "weapon_pistol", "name": "Pistol", "quantity": 1},
|
|
||||||
{"id": "item_medkit", "name": "Medkit", "quantity": 3}
|
|
||||||
],
|
|
||||||
"isOnline": false,
|
|
||||||
"lastSeen": "2025-01-01T12:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uuid": "123e4567-e89b-12d3-a456-426614174000",
|
|
||||||
"name": "TestPlayer",
|
|
||||||
"discordId": null,
|
|
||||||
"storyMarkdown": "# Test Player\n\nThis is a test player for mock data purposes.",
|
|
||||||
"tags": ["player"],
|
|
||||||
"stats": {
|
|
||||||
"health": 80,
|
|
||||||
"armor": 30,
|
|
||||||
"money": 5000
|
|
||||||
},
|
|
||||||
"inventory": [
|
|
||||||
{"id": "item_food", "name": "Food", "quantity": 5}
|
|
||||||
],
|
|
||||||
"isOnline": true,
|
|
||||||
"lastSeen": "2025-01-01T10:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Mock Project",
|
|
||||||
"description": "This is a mock project that appears when the backend is unavailable.",
|
|
||||||
"leader": "DrKButz",
|
|
||||||
"employees": ["8984c0b5-d912-4462-b189-c864fba4a1af"],
|
|
||||||
"shopCatalog": [],
|
|
||||||
"gallery": [],
|
|
||||||
"hiring": false,
|
|
||||||
"status": "active"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
import { MOCK_PLAYERS, MOCK_ORGS, MOCK_PROJECTS } from '../constants';
|
|
||||||
import { Player, Organization, Project } from '../types';
|
import { Player, Organization, Project } from '../types';
|
||||||
|
|
||||||
const API_URL = 'https://vollidioten.ceraticsoft.de/api';
|
const API_URL = 'https://vollidioten.ceraticsoft.de/api';
|
||||||
const MOCK_DATA_HEADER = 'X-Mock-Data';
|
|
||||||
|
|
||||||
class DatabaseService {
|
class DatabaseService {
|
||||||
private players: Player[] = MOCK_PLAYERS;
|
private players: Player[] = [];
|
||||||
private orgs: Organization[] = MOCK_ORGS;
|
private orgs: Organization[] = [];
|
||||||
private projects: Project[] = MOCK_PROJECTS;
|
private projects: Project[] = [];
|
||||||
private listeners: Function[] = [];
|
private listeners: Function[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.fetchAll();
|
this.fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to fetch real data from backend
|
// Fetch data from backend - always try to load fresh data
|
||||||
async fetchAll() {
|
async fetchAll() {
|
||||||
try {
|
try {
|
||||||
console.log("Fetching data from API...");
|
console.log("Fetching data from API...");
|
||||||
@@ -24,11 +22,6 @@ class DatabaseService {
|
|||||||
fetch(`${API_URL}/projects`)
|
fetch(`${API_URL}/projects`)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if we're getting mock data from nginx fallback
|
|
||||||
const isMockData = pRes.headers.get(MOCK_DATA_HEADER) === 'true' ||
|
|
||||||
oRes.headers.get(MOCK_DATA_HEADER) === 'true' ||
|
|
||||||
prRes.headers.get(MOCK_DATA_HEADER) === 'true';
|
|
||||||
|
|
||||||
if (pRes.ok && oRes.ok && prRes.ok) {
|
if (pRes.ok && oRes.ok && prRes.ok) {
|
||||||
const playersData = await pRes.json();
|
const playersData = await pRes.json();
|
||||||
const orgsData = await oRes.json();
|
const orgsData = await oRes.json();
|
||||||
@@ -41,19 +34,14 @@ class DatabaseService {
|
|||||||
this.projects = projectsData;
|
this.projects = projectsData;
|
||||||
this.notify();
|
this.notify();
|
||||||
|
|
||||||
if (isMockData) {
|
|
||||||
console.warn("Backend unavailable - using mock data from nginx fallback");
|
|
||||||
} else {
|
|
||||||
console.log("✅ Connected to Backend Database - using real data");
|
console.log("✅ Connected to Backend Database - using real data");
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`API returned errors: players=${pRes.status}, orgs=${oRes.status}, projects=${prRes.status}`);
|
console.warn(`API returned errors: players=${pRes.status}, orgs=${oRes.status}, projects=${prRes.status}`);
|
||||||
console.warn("Using built-in mock data");
|
throw new Error("Backend unavailable");
|
||||||
// Data is already set to mock data in constructor
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Backend not available, using built-in Mock Data.", e);
|
console.warn("Backend not available, data not loaded.", e);
|
||||||
// Fallback is already set in constructor
|
throw e; // Re-throw so components can handle the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user