mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
feat: Add DatabaseManager and LinkPlayer components, implement authentication and linking logic
- Created DatabaseManager component for managing database access via phpMyAdmin. - Developed LinkPlayer component to link Discord accounts with game characters, including user authentication and error handling. - Added mock data files for players, organizations, and projects to handle backend unavailability. - Implemented AuthService for managing user authentication and session checks. - Created DatabaseService to fetch and manage player, organization, and project data with fallback to mock data. - Added HTML page for handling authentication unavailability. - Developed a test script for validating Docker setup and required files.
This commit is contained in:
1251
pages/Admin.tsx
Normal file
1251
pages/Admin.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { MOCK_ORGS } from '../constants';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Organization } from '../types';
|
||||
import { Icons } from '../components/IconSet';
|
||||
import { dbService } from '../services/DatabaseService';
|
||||
|
||||
interface CitiesProps {
|
||||
onSelectCity: (id: string) => void;
|
||||
@@ -48,7 +48,55 @@ const CityCard = ({ city, onClick }: { city: Organization; onClick: () => void }
|
||||
);
|
||||
|
||||
const Cities: React.FC<CitiesProps> = ({ onSelectCity }) => {
|
||||
const cities = MOCK_ORGS.filter(org => org.type === 'City');
|
||||
const [cities, setCities] = useState<Organization[]>([]);
|
||||
const [citiesWithStats, setCitiesWithStats] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadCities = () => {
|
||||
// Get all organizations from database
|
||||
const allOrgs = dbService.getOrgs();
|
||||
const cityOrgs = allOrgs.filter(org => org.type === 'City');
|
||||
|
||||
// Calculate dynamic stats for each city
|
||||
const citiesStats = cityOrgs.map(city => {
|
||||
// Count citizens (players with this organizationId)
|
||||
const allPlayers = dbService.getPlayers();
|
||||
const citizenCount = allPlayers.filter(player =>
|
||||
player.stats.organizationId === city.id
|
||||
).length;
|
||||
|
||||
// Count businesses/projects in this city
|
||||
const allProjects = dbService.getProjects();
|
||||
const businessCount = allProjects.filter(project =>
|
||||
project.associatedOrgId === city.id
|
||||
).length;
|
||||
|
||||
return {
|
||||
...city,
|
||||
memberCount: citizenCount,
|
||||
businessCount: businessCount,
|
||||
// Override static memberCount with dynamic count
|
||||
stats: {
|
||||
citizens: citizenCount,
|
||||
businesses: businessCount,
|
||||
...city.cityStats
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
setCities(cityOrgs);
|
||||
setCitiesWithStats(citiesStats);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// Initial load
|
||||
loadCities();
|
||||
|
||||
// Subscribe to updates
|
||||
const unsub = dbService.subscribe(loadCities);
|
||||
return unsub;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-2 space-y-8">
|
||||
@@ -57,19 +105,29 @@ const Cities: React.FC<CitiesProps> = ({ onSelectCity }) => {
|
||||
<p className="text-textMuted">Entdecke die blühenden Zentren der Zivilisation im Obsidian-Tal.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{cities.map(city => (
|
||||
<CityCard key={city.id} city={city} onClick={() => onSelectCity(city.id)} />
|
||||
))}
|
||||
</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 gap-6">
|
||||
{citiesWithStats.map(city => (
|
||||
<div key={city.id}>
|
||||
<CityCard city={city} onClick={() => onSelectCity(city.id)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{cities.length === 0 && (
|
||||
<div className="text-center py-20 text-textMuted">
|
||||
<p>Noch keine Städte gegründet.</p>
|
||||
</div>
|
||||
{citiesWithStats.length === 0 && (
|
||||
<div className="text-center py-20 text-textMuted">
|
||||
<p>Noch keine Städte gegründet.</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cities;
|
||||
export default Cities;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Organization, Project, Player } from '../types';
|
||||
import { MOCK_PLAYERS, MOCK_PROJECTS } from '../constants';
|
||||
import { Icons } from '../components/IconSet';
|
||||
import { dbService } from '../services/DatabaseService';
|
||||
|
||||
interface CityProfileProps {
|
||||
city: Organization;
|
||||
@@ -11,17 +11,52 @@ interface CityProfileProps {
|
||||
onSelectProject: (id: string) => void;
|
||||
}
|
||||
|
||||
const CityProfile: React.FC<CityProfileProps> = ({
|
||||
city,
|
||||
onBack,
|
||||
const CityProfile: React.FC<CityProfileProps> = ({
|
||||
city,
|
||||
onBack,
|
||||
backLabel = 'Zurück',
|
||||
onSelectPlayer,
|
||||
onSelectProject
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'residents' | 'ventures'>('overview');
|
||||
|
||||
const residents = MOCK_PLAYERS.filter(p => p.stats.organizationId === city.id);
|
||||
const ventures = MOCK_PROJECTS.filter(p => p.associatedOrgId === city.id);
|
||||
const [residents, setResidents] = useState<Player[]>([]);
|
||||
const [ventures, setVentures] = useState<Project[]>([]);
|
||||
const [cityStats, setCityStats] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// Initial load
|
||||
loadCityData();
|
||||
|
||||
// Subscribe to updates
|
||||
const unsub = dbService.subscribe(loadCityData);
|
||||
return unsub;
|
||||
}, [city.id]);
|
||||
|
||||
return (
|
||||
<div className="animate-in slide-in-from-right-4 duration-300">
|
||||
@@ -244,4 +279,4 @@ const CityProfile: React.FC<CityProfileProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CityProfile;
|
||||
export default CityProfile;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { MOCK_PLAYERS, MOCK_PROJECTS, MOCK_ORGS } from '../constants';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { dbService } from '../services/DatabaseService';
|
||||
import { Icons } from '../components/IconSet';
|
||||
import { Player, Project, Organization } from '../types';
|
||||
|
||||
const StatCard = ({ label, value, trend, icon: Icon }: any) => (
|
||||
<div className="bg-surface/50 border border-border p-6 rounded-xl hover:border-accentInfo/30 transition-all duration-300 group">
|
||||
@@ -41,6 +42,31 @@ const ProjectCard = ({ project }: { project: any }) => (
|
||||
);
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const [players, setPlayers] = useState<Player[]>([]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [orgs, setOrgs] = useState<Organization[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Subscribe to database updates
|
||||
const unsubscribe = dbService.subscribe(() => {
|
||||
setPlayers(dbService.getPlayers());
|
||||
setProjects(dbService.getProjects());
|
||||
setOrgs(dbService.getOrgs());
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
// Initial data load
|
||||
setPlayers(dbService.getPlayers());
|
||||
setProjects(dbService.getProjects());
|
||||
setOrgs(dbService.getOrgs());
|
||||
setLoading(false);
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const activeProjectsCount = projects.filter(p => p.status === 'active' || p.status === 'recruiting').length;
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{/* Intro Section */}
|
||||
@@ -56,9 +82,9 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
{/* KPI Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<StatCard label="Registrierte Bürger" value={MOCK_PLAYERS.length} trend={12} icon={Icons.Users} />
|
||||
<StatCard label="Aktive Unternehmen" value={MOCK_PROJECTS.filter(p => p.status === 'active' || p.status === 'recruiting').length} trend={5} icon={Icons.Layers} />
|
||||
<StatCard label="Organisationen" value={MOCK_ORGS.length} trend={0} icon={Icons.Map} />
|
||||
<StatCard label="Registrierte Bürger" value={loading ? '...' : players.length} trend={12} icon={Icons.Users} />
|
||||
<StatCard label="Aktive Unternehmen" value={loading ? '...' : activeProjectsCount} trend={5} icon={Icons.Layers} />
|
||||
<StatCard label="Organisationen" value={loading ? '...' : orgs.length} trend={0} icon={Icons.Map} />
|
||||
</div>
|
||||
|
||||
{/* Content Grid */}
|
||||
@@ -70,7 +96,17 @@ const Dashboard: React.FC = () => {
|
||||
<button className="text-sm text-textMuted hover:text-accentInfo transition-colors">Verzeichnis ansehen →</button>
|
||||
</div>
|
||||
<div className="bg-surface/30 border border-border rounded-2xl p-6 backdrop-blur-sm">
|
||||
{MOCK_PROJECTS.slice(0, 5).map(p => <ProjectCard key={p.id} project={p} />)}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accentInfo"></div>
|
||||
</div>
|
||||
) : (
|
||||
projects.slice(0, 5).map(p => (
|
||||
<div key={p.id}>
|
||||
<ProjectCard project={p} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,4 +144,4 @@ const Dashboard: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Dashboard;
|
||||
|
||||
25
pages/DatabaseManager.tsx
Normal file
25
pages/DatabaseManager.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
const DatabaseManager: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center p-10 animate-in fade-in">
|
||||
<div className="bg-surface border border-border rounded-xl p-8 max-w-md shadow-card">
|
||||
<h1 className="text-2xl font-bold mb-4 text-textMain">Datenbank-Manager</h1>
|
||||
<p className="text-textMuted mb-6 leading-relaxed">
|
||||
Die Datenbank wird über eine externe MySQL-Instanz verwaltet.
|
||||
Für administrative Eingriffe nutze bitte das phpMyAdmin Interface.
|
||||
</p>
|
||||
<a
|
||||
href="http://localhost:8081"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center px-6 py-3 bg-accentInfo text-white rounded-lg hover:bg-accentInfo/90 transition-colors font-medium shadow-glow"
|
||||
>
|
||||
phpMyAdmin öffnen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatabaseManager;
|
||||
201
pages/LinkPlayer.tsx
Normal file
201
pages/LinkPlayer.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { authService } from '../services/AuthService';
|
||||
import { DiscordUser } from '../types';
|
||||
|
||||
interface Player {
|
||||
uuid: string;
|
||||
username: string;
|
||||
tags: string[];
|
||||
stats: {
|
||||
playtimeHours: number;
|
||||
level: number;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
const LinkPlayer: React.FC = () => {
|
||||
const [user, setUser] = useState<DiscordUser | null>(null);
|
||||
const [unlinkedPlayers, setUnlinkedPlayers] = useState<Player[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [linking, setLinking] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Get current user
|
||||
const unsubscribe = authService.subscribe(setUser);
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch unlinked players
|
||||
if (user) {
|
||||
fetchUnlinkedPlayers();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchUnlinkedPlayers = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('https://vollidioten.ceraticsoft.de/api/unlinked-players');
|
||||
if (response.ok) {
|
||||
const players = await response.json();
|
||||
setUnlinkedPlayers(players);
|
||||
} else {
|
||||
setError('Fehler beim Laden der Spielerliste');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching unlinked players:', err);
|
||||
setError('Netzwerkfehler beim Laden der Spielerliste');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const linkPlayer = async (playerUuid: string) => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
setLinking(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch('https://vollidioten.ceraticsoft.de/api/link-user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ playerUuid }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update user data
|
||||
const updatedUser = { ...user, linkedPlayerUuid: playerUuid };
|
||||
setUser(updatedUser);
|
||||
|
||||
// Redirect to dashboard
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
setError('Fehler beim Verknüpfen des Accounts');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error linking player:', err);
|
||||
setError('Netzwerkfehler beim Verknüpfen');
|
||||
} finally {
|
||||
setLinking(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold mb-4">Nicht eingeloggt</h2>
|
||||
<p>Bitte loggen Sie sich zuerst ein.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (user.linkedPlayerUuid) {
|
||||
// User is already linked, redirect to dashboard
|
||||
window.location.href = '/';
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background py-12 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt={user.username}
|
||||
className="w-16 h-16 rounded-full border-2 border-accentInfo"
|
||||
/>
|
||||
<div className="text-left">
|
||||
<h1 className="text-3xl font-bold text-textMain">Willkommen, {user.username}!</h1>
|
||||
<p className="text-textMuted">Verbinde deinen Discord-Account mit einem Bürger</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface rounded-lg p-6 border border-border">
|
||||
<h2 className="text-xl font-semibold mb-4 text-textMain">Wähle deinen Bürger</h2>
|
||||
<p className="text-textMuted mb-6">
|
||||
Wähle einen Bürger aus der Liste der verfügbaren Charaktere aus, um deinen Discord-Account zu verknüpfen.
|
||||
</p>
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accentInfo"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{unlinkedPlayers.map((player) => (
|
||||
<div
|
||||
key={player.uuid}
|
||||
className="bg-surfaceHighlight border border-border rounded-lg p-4 hover:border-accentInfo transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-accentInfo to-blue-900 rounded-full flex items-center justify-center">
|
||||
<span className="font-bold text-white text-sm">
|
||||
{player.username.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-textMain">{player.username}</h3>
|
||||
<p className="text-sm text-textMuted">Level {player.stats.level}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{player.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="text-xs bg-accentInfo/10 text-accentInfo px-2 py-1 rounded"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-textMuted mb-4">
|
||||
<p>Spielzeit: {player.stats.playtimeHours}h</p>
|
||||
<p>Rolle: {player.stats.role}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => linkPlayer(player.uuid)}
|
||||
disabled={linking}
|
||||
className="w-full bg-accentInfo hover:bg-accentInfo/80 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium py-2 px-4 rounded transition-colors"
|
||||
>
|
||||
{linking ? 'Verknüpfe...' : 'Diesen Bürger wählen'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unlinkedPlayers.length === 0 && !loading && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-textMuted">Keine verfügbaren Bürger gefunden.</p>
|
||||
<p className="text-sm text-textMuted mt-2">
|
||||
Alle Bürger sind bereits verknüpft oder es gibt einen Fehler beim Laden.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkPlayer;
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Player } from '../types';
|
||||
import { MOCK_ORGS } from '../constants';
|
||||
import { dbService } from '../services/DatabaseService';
|
||||
import { authService } from '../services/AuthService';
|
||||
import InventoryGrid from '../components/InventoryGrid';
|
||||
import EditModal from '../components/EditModal';
|
||||
import { Icons } from '../components/IconSet';
|
||||
|
||||
interface PlayerProfileProps {
|
||||
@@ -9,11 +11,95 @@ interface PlayerProfileProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const PlayerProfile: React.FC<PlayerProfileProps> = ({ player, onBack }) => {
|
||||
const playerOrg = MOCK_ORGS.find(o => o.id === player.stats.organizationId);
|
||||
const PlayerProfile: React.FC<PlayerProfileProps> = ({ player: initialPlayer, onBack }) => {
|
||||
const [player, setPlayer] = useState(initialPlayer);
|
||||
const [currentUser, setCurrentUser] = useState(authService.getUser());
|
||||
|
||||
// Edit State
|
||||
const [isEditStoryOpen, setIsEditStoryOpen] = useState(false);
|
||||
const [isEditTagsOpen, setIsEditTagsOpen] = useState(false);
|
||||
const [isEditOrgOpen, setIsEditOrgOpen] = useState(false);
|
||||
const [ownedProjects, setOwnedProjects] = useState<any[]>([]);
|
||||
|
||||
// Is this the logged-in user's profile?
|
||||
const isOwner = currentUser?.linkedPlayerUuid === player.uuid;
|
||||
const playerOrg = dbService.getOrg(player.stats.organizationId || '');
|
||||
|
||||
// Check if player is already linked to anyone in our mock/real DB logic
|
||||
// Since the Player object doesn't expose 'discordId' publicly in types yet (it's hidden in DB),
|
||||
// we use a heuristic or add it. For now, we assume if currentUser has NO link, they can claim.
|
||||
const canClaim = !!currentUser && !currentUser.linkedPlayerUuid && !isOwner;
|
||||
|
||||
useEffect(() => {
|
||||
// Refresh player data from "DB" to ensure we have latest local edits
|
||||
const freshData = dbService.getPlayer(player.uuid);
|
||||
if (freshData) setPlayer(freshData);
|
||||
|
||||
// Load owned projects
|
||||
const allProjects = dbService.getProjects();
|
||||
const playerProjects = allProjects.filter(p => p.owner === player.username);
|
||||
setOwnedProjects(playerProjects);
|
||||
|
||||
// Subscribe to auth to show/hide edit buttons
|
||||
const unsub = authService.subscribe(u => setCurrentUser(u));
|
||||
return unsub;
|
||||
}, [player.uuid, player.username]);
|
||||
|
||||
const handleSaveStory = (newStory: string) => {
|
||||
// Update local DB
|
||||
dbService.updatePlayer(player.uuid, { storyMarkdown: newStory });
|
||||
// Update local state
|
||||
setPlayer(prev => ({ ...prev, storyMarkdown: newStory }));
|
||||
};
|
||||
|
||||
const handleSaveTags = async (tagsString: string) => {
|
||||
const tags = tagsString.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
|
||||
// Update via API
|
||||
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/players/${player.uuid}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ tags })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update local state
|
||||
setPlayer(prev => ({ ...prev, tags }));
|
||||
} else {
|
||||
alert('Fehler beim Aktualisieren der Tags');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveOrganization = async (orgId: string) => {
|
||||
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/players/${player.uuid}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ organizationId: orgId || null })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update local state
|
||||
setPlayer(prev => ({ ...prev, stats: { ...prev.stats, organizationId: orgId || undefined } }));
|
||||
} else {
|
||||
alert('Fehler beim Aktualisieren der Zugehörigkeit');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClaim = async () => {
|
||||
if (confirm(`Möchtest du den Charakter "${player.username}" mit deinem Discord-Account verknüpfen?`)) {
|
||||
const success = await dbService.linkPlayer(player.uuid);
|
||||
if (success) {
|
||||
// Force refresh auth to get updated linkedPlayerUuid
|
||||
await authService.checkSession();
|
||||
alert("Erfolgreich verknüpft! Du kannst nun Bearbeitungen vornehmen.");
|
||||
} else {
|
||||
alert("Fehler beim Verknüpfen. Bitte Backend Logs prüfen.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Simple markdown renderer replacement for demo purposes
|
||||
// In production, use 'react-markdown'
|
||||
const renderMarkdown = (text: string) => {
|
||||
return text.split('\n').map((line, i) => {
|
||||
if (line.startsWith('# ')) return <h1 key={i} className="text-2xl font-bold mt-6 mb-3 text-textMain border-b border-border pb-2">{line.replace('# ', '')}</h1>;
|
||||
@@ -26,15 +112,37 @@ const PlayerProfile: React.FC<PlayerProfileProps> = ({ player, onBack }) => {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto animate-in slide-in-from-right-4 duration-300">
|
||||
<button onClick={onBack} className="flex items-center gap-2 text-sm text-textMuted hover:text-textMain mb-6 transition-colors">
|
||||
<span className="text-lg">←</span> Zurück zur Liste
|
||||
</button>
|
||||
<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 zur Liste
|
||||
</button>
|
||||
{canClaim && (
|
||||
<button
|
||||
onClick={handleClaim}
|
||||
className="text-xs bg-accentInfo hover:bg-accentInfo/80 text-white px-3 py-1.5 rounded flex items-center gap-2 transition-colors shadow-glow animate-pulse"
|
||||
>
|
||||
<Icons.Shield className="w-3 h-3" />
|
||||
Charakter beanspruchen (Test)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="bg-surface border border-border rounded-xl p-6 shadow-card mb-6">
|
||||
<div className="bg-surface border border-border rounded-xl p-6 shadow-card mb-6 relative overflow-hidden">
|
||||
{isOwner && (
|
||||
<div className="absolute top-0 right-0 bg-accentInfo text-white text-xs font-bold px-3 py-1 rounded-bl-xl shadow-lg flex items-center gap-1 z-10">
|
||||
<Icons.Edit className="w-3 h-3" /> Dein Profil
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6 items-start md:items-center">
|
||||
<div className="w-20 h-20 rounded-lg flex items-center justify-center shadow-inner shrink-0">
|
||||
<img src={"https://minotar.net/armor/bust/"+player.username+"/500.png"}></img>
|
||||
<div className="w-20 h-20 rounded-lg flex items-center justify-center shadow-inner shrink-0 relative group">
|
||||
<img src={"https://minotar.net/armor/bust/"+player.username+"/500.png"} className="rounded-lg shadow-lg"></img>
|
||||
{isOwner && (
|
||||
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center rounded-lg cursor-pointer">
|
||||
<span className="text-[10px] text-white font-bold">Ändern</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
@@ -54,6 +162,14 @@ const PlayerProfile: React.FC<PlayerProfileProps> = ({ player, onBack }) => {
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{isOwner && (
|
||||
<button
|
||||
onClick={() => setIsEditTagsOpen(true)}
|
||||
className="text-xs px-2 py-1 border border-dashed border-textMuted/50 text-textMuted hover:text-white hover:border-white/50 rounded transition-colors"
|
||||
>
|
||||
+ Tag bearbeiten
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 text-sm">
|
||||
@@ -71,9 +187,9 @@ const PlayerProfile: React.FC<PlayerProfileProps> = ({ player, onBack }) => {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left Col: Inventory */}
|
||||
<div className="lg:col-span-1">
|
||||
{/* <InventoryGrid items={player.inventory} /> */}
|
||||
{/* Left Col: Inventory & Org */}
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
<InventoryGrid items={player.inventory} />
|
||||
|
||||
<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>
|
||||
@@ -98,31 +214,99 @@ const PlayerProfile: React.FC<PlayerProfileProps> = ({ player, onBack }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{playerOrg && (
|
||||
<div className="mt-3 pt-3 border-t border-white/5 text-xs text-textMuted leading-relaxed">
|
||||
{playerOrg.type} • {playerOrg.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Col: Story */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-surface border border-border rounded-xl p-8 shadow-card min-h-[400px]">
|
||||
{/* Right Col: Story & Projects */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Story Section */}
|
||||
<div className="bg-surface border border-border rounded-xl p-8 shadow-card min-h-[400px] relative">
|
||||
<div className="flex items-center justify-between mb-6 border-b border-border pb-4">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
Charakter-Journal
|
||||
</h2>
|
||||
<span className="text-xs text-textMuted font-mono">Markdown Rendered</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-textMuted font-mono">Markdown</span>
|
||||
{isOwner && (
|
||||
<button
|
||||
onClick={() => setIsEditStoryOpen(true)}
|
||||
className="flex items-center gap-1.5 text-xs bg-surfaceHighlight hover:bg-white/10 border border-white/10 px-2 py-1 rounded transition-colors text-white"
|
||||
>
|
||||
<Icons.Edit className="w-3 h-3" /> Bearbeiten
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose-custom text-sm">
|
||||
{renderMarkdown(player.storyMarkdown)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Owned Projects Section */}
|
||||
{ownedProjects.length > 0 && (
|
||||
<div className="bg-surface border border-border rounded-xl p-6 shadow-card">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Icons.ShoppingBag className="w-5 h-5 text-accentInfo" />
|
||||
Unternehmen & Projekte ({ownedProjects.length})
|
||||
</h3>
|
||||
<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 className="flex justify-between items-start mb-2">
|
||||
<h4 className="font-medium text-textMain">{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' :
|
||||
'bg-gray-500/10 text-gray-400 border-gray-500/20'
|
||||
}`}>
|
||||
{project.category}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-textMuted mb-3 line-clamp-2">{project.description}</p>
|
||||
<div className="flex items-center justify-between text-xs text-textMuted">
|
||||
<span>Gegründet: {project.foundedDate}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{project.shopCatalog && project.shopCatalog.length > 0 && (
|
||||
<span className="flex items-center gap-1 text-accentInfo">
|
||||
<Icons.ShoppingBag className="w-3 h-3" />
|
||||
Shop ({project.shopCatalog.length})
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<Icons.Users className="w-3 h-3" />
|
||||
{project.employees.length + 1}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<EditModal
|
||||
isOpen={isEditStoryOpen}
|
||||
title="Journal bearbeiten"
|
||||
initialValue={player.storyMarkdown}
|
||||
multiline={true}
|
||||
markdown={true}
|
||||
onClose={() => setIsEditStoryOpen(false)}
|
||||
onSave={handleSaveStory}
|
||||
/>
|
||||
|
||||
<EditModal
|
||||
isOpen={isEditTagsOpen}
|
||||
title="Tags bearbeiten"
|
||||
initialValue={player.tags.join(', ')}
|
||||
multiline={false}
|
||||
onClose={() => setIsEditTagsOpen(false)}
|
||||
onSave={handleSaveTags}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayerProfile;
|
||||
export default PlayerProfile;
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Project, ShopItem } from '../types';
|
||||
import { MOCK_ORGS, MOCK_PLAYERS } from '../constants';
|
||||
import { Icons, ItemIcon } from '../components/IconSet';
|
||||
import { dbService } from '../services/DatabaseService';
|
||||
import { authService } from '../services/AuthService';
|
||||
import { DiscordUser } from '../types';
|
||||
import EditModal from '../components/EditModal';
|
||||
import ShopManagementModal from '../components/ShopManagementModal';
|
||||
import EmployeeManagementModal from '../components/EmployeeManagementModal';
|
||||
import BannerManagementModal from '../components/BannerManagementModal';
|
||||
import GalleryManagementModal from '../components/GalleryManagementModal';
|
||||
import DeleteProjectModal from '../components/DeleteProjectModal';
|
||||
|
||||
interface ProjectProfileProps {
|
||||
project: Project;
|
||||
@@ -10,18 +18,46 @@ interface ProjectProfileProps {
|
||||
onSelectOrg: (id: string) => void;
|
||||
}
|
||||
|
||||
const ProjectProfile: React.FC<ProjectProfileProps> = ({
|
||||
project,
|
||||
const ProjectProfile: React.FC<ProjectProfileProps> = ({
|
||||
project,
|
||||
onBack,
|
||||
onSelectPlayer,
|
||||
onSelectOrg
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'shop'>('overview');
|
||||
|
||||
const org = project.associatedOrgId ? MOCK_ORGS.find(o => o.id === project.associatedOrgId) : null;
|
||||
const ownerPlayer = MOCK_PLAYERS.find(p => p.username === project.owner);
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'shop' | 'manage'>('overview');
|
||||
const [user, setUser] = useState<DiscordUser | null>(null);
|
||||
const [org, setOrg] = useState<any>(null);
|
||||
const [ownerPlayer, setOwnerPlayer] = useState<any>(null);
|
||||
const [isEditDescriptionOpen, setIsEditDescriptionOpen] = useState(false);
|
||||
const [isEditHiringOpen, setIsEditHiringOpen] = useState(false);
|
||||
const [isShopModalOpen, setIsShopModalOpen] = useState(false);
|
||||
const [isEmployeeModalOpen, setIsEmployeeModalOpen] = useState(false);
|
||||
const [isBannerModalOpen, setIsBannerModalOpen] = useState(false);
|
||||
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
// Subscribe to auth and data updates
|
||||
useEffect(() => {
|
||||
const unsubAuth = authService.subscribe(setUser);
|
||||
return unsubAuth;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Load associated org and owner player
|
||||
if (project.associatedOrgId) {
|
||||
const foundOrg = dbService.getOrg(project.associatedOrgId);
|
||||
setOrg(foundOrg);
|
||||
}
|
||||
|
||||
// Find owner player by username
|
||||
const players = dbService.getPlayers();
|
||||
const owner = players.find(p => p.username === project.owner);
|
||||
setOwnerPlayer(owner);
|
||||
}, [project]);
|
||||
|
||||
const hasShop = project.shopCatalog && project.shopCatalog.length > 0;
|
||||
|
||||
const isOwner = user?.linkedPlayerUuid && ownerPlayer && dbService.getPlayer(user.linkedPlayerUuid)?.username === project.owner;
|
||||
|
||||
// Group shop items
|
||||
const services = project.shopCatalog?.filter(i => i.type === 'service') || [];
|
||||
const products = project.shopCatalog?.filter(i => i.type !== 'service') || [];
|
||||
@@ -105,20 +141,28 @@ const ProjectProfile: React.FC<ProjectProfileProps> = ({
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex gap-8 border-b border-border mb-8 px-2">
|
||||
<button
|
||||
<button
|
||||
onClick={() => setActiveTab('overview')}
|
||||
className={`pb-4 text-sm font-medium transition-colors border-b-2 ${activeTab === 'overview' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||||
>
|
||||
Übersicht
|
||||
</button>
|
||||
{hasShop && (
|
||||
<button
|
||||
<button
|
||||
onClick={() => setActiveTab('shop')}
|
||||
className={`pb-4 text-sm font-medium transition-colors border-b-2 ${activeTab === 'shop' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||||
>
|
||||
Katalog ({project.shopCatalog?.length})
|
||||
</button>
|
||||
)}
|
||||
{isOwner && (
|
||||
<button
|
||||
onClick={() => setActiveTab('manage')}
|
||||
className={`pb-4 text-sm font-medium transition-colors border-b-2 ${activeTab === 'manage' ? 'border-accentInfo text-white' : 'border-transparent text-textMuted hover:text-white'}`}
|
||||
>
|
||||
Verwalten
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
@@ -156,10 +200,11 @@ const ProjectProfile: React.FC<ProjectProfileProps> = ({
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{project.employees.map((emp, idx) => {
|
||||
const empPlayer = MOCK_PLAYERS.find(p => p.username === emp);
|
||||
const players = dbService.getPlayers();
|
||||
const empPlayer = players.find(p => p.username === emp);
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => empPlayer && onSelectPlayer(empPlayer.uuid)}
|
||||
className={`flex items-center gap-3 bg-surface border border-border p-3 rounded-lg ${empPlayer ? 'cursor-pointer hover:border-accentInfo/50 hover:bg-surfaceHighlight/30 transition-all group' : ''}`}
|
||||
>
|
||||
@@ -247,6 +292,117 @@ const ProjectProfile: React.FC<ProjectProfileProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'manage' && isOwner && (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-surface/50 border border-border rounded-xl p-6">
|
||||
<h3 className="text-lg font-bold text-white mb-6 flex items-center gap-2">
|
||||
<Icons.Edit className="w-5 h-5 text-accentInfo" />
|
||||
Unternehmen verwalten
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Edit Description */}
|
||||
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-textMain mb-3">Manifest bearbeiten</h4>
|
||||
<p className="text-xs text-textMuted mb-4">
|
||||
Ändern Sie die Beschreibung und das Leitbild Ihres Unternehmens.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsEditDescriptionOpen(true)}
|
||||
className="w-full bg-accentInfo hover:bg-accentInfo/90 text-white text-sm font-medium py-2 px-4 rounded transition-colors"
|
||||
>
|
||||
Beschreibung bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Toggle Hiring */}
|
||||
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-textMain mb-3">Stellenanzeigen</h4>
|
||||
<p className="text-xs text-textMuted mb-4">
|
||||
Aktuell: {project.hiring ? 'Stellen sind ausgeschrieben' : 'Keine offenen Stellen'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsEditHiringOpen(true)}
|
||||
className="w-full bg-accentInfo hover:bg-accentInfo/90 text-white text-sm font-medium py-2 px-4 rounded transition-colors"
|
||||
>
|
||||
{project.hiring ? 'Stellenanzeigen beenden' : 'Stellen ausschreiben'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Shop Management */}
|
||||
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-textMain mb-3">Shop verwalten</h4>
|
||||
<p className="text-xs text-textMuted mb-4">
|
||||
Fügen Sie Produkte, Dienstleistungen und Preise hinzu.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsShopModalOpen(true)}
|
||||
className="w-full bg-purple-500 hover:bg-purple-600 text-white text-sm font-medium py-2 px-4 rounded transition-colors"
|
||||
>
|
||||
Shop bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Employee Management */}
|
||||
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-textMain mb-3">Mitarbeiter verwalten</h4>
|
||||
<p className="text-xs text-textMuted mb-4">
|
||||
Fügen Sie Mitarbeiter hinzu oder entfernen Sie sie.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsEmployeeModalOpen(true)}
|
||||
className="w-full bg-green-500 hover:bg-green-600 text-white text-sm font-medium py-2 px-4 rounded transition-colors"
|
||||
>
|
||||
Mitarbeiter verwalten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Banner Management */}
|
||||
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-textMain mb-3">Banner ändern</h4>
|
||||
<p className="text-xs text-textMuted mb-4">
|
||||
Ändern Sie das Titelbild Ihres Unternehmens.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsBannerModalOpen(true)}
|
||||
className="w-full bg-orange-500 hover:bg-orange-600 text-white text-sm font-medium py-2 px-4 rounded transition-colors"
|
||||
>
|
||||
Banner bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Gallery Management */}
|
||||
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-textMain mb-3">Galerie verwalten</h4>
|
||||
<p className="text-xs text-textMuted mb-4">
|
||||
Fügen Sie Bilder zu Ihrem Portfolio hinzu.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsGalleryModalOpen(true)}
|
||||
className="w-full bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium py-2 px-4 rounded transition-colors"
|
||||
>
|
||||
Galerie bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-red-400 mb-3">Gefahrenzone</h4>
|
||||
<p className="text-xs text-red-300 mb-4">
|
||||
Unwiderrufliche Aktionen für dieses Unternehmen.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="w-full bg-red-500 hover:bg-red-600 text-white text-sm font-medium py-2 px-4 rounded transition-colors"
|
||||
>
|
||||
Unternehmen löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'shop' && project.shopCatalog && (
|
||||
<div className="space-y-12">
|
||||
|
||||
@@ -342,9 +498,141 @@ const ProjectProfile: React.FC<ProjectProfileProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Modals */}
|
||||
<EditModal
|
||||
isOpen={isEditDescriptionOpen}
|
||||
title="Manifest bearbeiten"
|
||||
initialValue={project.description}
|
||||
multiline={true}
|
||||
markdown={true}
|
||||
onClose={() => setIsEditDescriptionOpen(false)}
|
||||
onSave={async (newDescription) => {
|
||||
try {
|
||||
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${project.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ description: newDescription })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update local data
|
||||
dbService.updateProject(project.id, { description: newDescription });
|
||||
console.log('Description updated successfully');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`Fehler beim Aktualisieren: ${errorData.error || 'Unbekannter Fehler'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating description:', err);
|
||||
alert('Netzwerkfehler beim Aktualisieren der Beschreibung');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<EditModal
|
||||
isOpen={isEditHiringOpen}
|
||||
title={project.hiring ? 'Stellenanzeigen beenden' : 'Stellen ausschreiben'}
|
||||
initialValue={project.hiring ? 'Ja, Stellen sind derzeit ausgeschrieben.' : 'Nein, derzeit keine offenen Stellen.'}
|
||||
multiline={false}
|
||||
onClose={() => setIsEditHiringOpen(false)}
|
||||
onSave={async (value) => {
|
||||
const newHiringStatus = !project.hiring;
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${project.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ hiring: newHiringStatus })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update local data
|
||||
dbService.updateProject(project.id, { hiring: newHiringStatus });
|
||||
console.log('Hiring status updated successfully:', newHiringStatus);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`Fehler beim Aktualisieren: ${errorData.error || 'Unbekannter Fehler'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating hiring status:', err);
|
||||
alert('Netzwerkfehler beim Aktualisieren der Stellenanzeigen');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Management Modals */}
|
||||
<ShopManagementModal
|
||||
isOpen={isShopModalOpen}
|
||||
onClose={() => setIsShopModalOpen(false)}
|
||||
projectId={project.id}
|
||||
onUpdate={() => {
|
||||
// Refresh project data
|
||||
console.log('Shop updated, refreshing project data...');
|
||||
// The dbService should automatically update via subscription
|
||||
}}
|
||||
/>
|
||||
|
||||
<EmployeeManagementModal
|
||||
isOpen={isEmployeeModalOpen}
|
||||
onClose={() => setIsEmployeeModalOpen(false)}
|
||||
projectId={project.id}
|
||||
onUpdate={() => {
|
||||
// Refresh project data
|
||||
console.log('Employees updated, refreshing project data...');
|
||||
}}
|
||||
/>
|
||||
|
||||
<BannerManagementModal
|
||||
isOpen={isBannerModalOpen}
|
||||
onClose={() => setIsBannerModalOpen(false)}
|
||||
projectId={project.id}
|
||||
currentBannerUrl={project.bannerUrl || ''}
|
||||
onUpdate={() => {
|
||||
// Refresh project data
|
||||
console.log('Banner updated, refreshing project data...');
|
||||
}}
|
||||
/>
|
||||
|
||||
<GalleryManagementModal
|
||||
isOpen={isGalleryModalOpen}
|
||||
onClose={() => setIsGalleryModalOpen(false)}
|
||||
projectId={project.id}
|
||||
onUpdate={() => {
|
||||
// Refresh project data
|
||||
console.log('Gallery updated, refreshing project data...');
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteProjectModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
projectId={project.id}
|
||||
projectTitle={project.title}
|
||||
onDelete={async () => {
|
||||
try {
|
||||
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${project.id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Projekt erfolgreich gelöscht!');
|
||||
onBack(); // Navigate back to projects list
|
||||
} else {
|
||||
alert('Fehler beim Löschen des Projekts');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting project:', err);
|
||||
alert('Netzwerkfehler beim Löschen');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectProfile;
|
||||
export default ProjectProfile;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { MOCK_PROJECTS } from '../constants';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Project } from '../types';
|
||||
import { Icons } from '../components/IconSet';
|
||||
import { dbService } from '../services/DatabaseService';
|
||||
import { authService } from '../services/AuthService';
|
||||
import { DiscordUser } from '../types';
|
||||
import CreateProjectModal from '../components/CreateProjectModal';
|
||||
|
||||
interface ProjectsProps {
|
||||
onSelectProject?: (id: string) => void;
|
||||
@@ -113,11 +116,69 @@ const VentureCard = ({ project, onClick }: { project: Project, onClick?: () => v
|
||||
|
||||
const Projects: React.FC<ProjectsProps> = ({ onSelectProject }) => {
|
||||
const [filter, setFilter] = useState<'all' | Project['status']>('all');
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [user, setUser] = useState<DiscordUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
|
||||
const filteredProjects = MOCK_PROJECTS.filter(p =>
|
||||
// Subscribe to auth and data updates
|
||||
useEffect(() => {
|
||||
const unsubAuth = authService.subscribe(setUser);
|
||||
const unsubDb = dbService.subscribe(() => {
|
||||
setProjects(dbService.getProjects());
|
||||
});
|
||||
|
||||
// Initial data load
|
||||
setProjects(dbService.getProjects());
|
||||
setLoading(false);
|
||||
|
||||
return () => {
|
||||
unsubAuth();
|
||||
unsubDb();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filteredProjects = projects.filter(p =>
|
||||
filter === 'all' ? true : p.status === filter
|
||||
);
|
||||
|
||||
// Berechne den Namen des verknüpften Charakters für die Modal-Anzeige
|
||||
const getLinkedPlayerName = () => {
|
||||
if (!user) return null;
|
||||
if (user.linkedPlayerUuid) {
|
||||
const linkedPlayer = dbService.getPlayer(user.linkedPlayerUuid);
|
||||
return linkedPlayer ? linkedPlayer.username : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const linkedPlayerName = getLinkedPlayerName();
|
||||
|
||||
const createProject = async (projectData: { title: string; description: string; category: Project['category'] }) => {
|
||||
if (!user) {
|
||||
throw new Error('Nicht eingeloggt');
|
||||
}
|
||||
|
||||
console.log('Creating project with data:', projectData);
|
||||
console.log('User will be resolved to Minecraft name in backend');
|
||||
|
||||
const success = await dbService.createProject(projectData);
|
||||
|
||||
if (!success) {
|
||||
throw new Error('Fehler beim Erstellen des Projekts');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
if (!user) {
|
||||
// Redirect to login or show message
|
||||
alert('Sie müssen sich zuerst anmelden, um ein Unternehmen zu erstellen.');
|
||||
authService.login();
|
||||
return;
|
||||
}
|
||||
setShowCreateForm(true);
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'all', label: 'Alle Unternehmen' },
|
||||
{ id: 'active', label: 'Aktive Firmen' },
|
||||
@@ -132,7 +193,11 @@ const Projects: React.FC<ProjectsProps> = ({ onSelectProject }) => {
|
||||
<h1 className="text-3xl font-bold mb-1">Unternehmen & Projekte</h1>
|
||||
<p className="text-textMuted">Spielergeführte Firmen, aktive Rollenspiel-Stränge und Dienstleister.</p>
|
||||
</div>
|
||||
<button className="bg-textMain text-background hover:bg-white font-medium px-4 py-2 rounded-lg text-sm transition-colors flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCreateClick}
|
||||
className="bg-textMain text-background hover:bg-white font-medium px-4 py-2 rounded-lg text-sm transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Icons.ShoppingBag className="w-4 h-4" />
|
||||
<span>+ Firma registrieren</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -157,11 +222,12 @@ const Projects: React.FC<ProjectsProps> = ({ onSelectProject }) => {
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredProjects.map(project => (
|
||||
<VentureCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
onClick={() => onSelectProject && onSelectProject(project.id)}
|
||||
/>
|
||||
<div key={project.id}>
|
||||
<VentureCard
|
||||
project={project}
|
||||
onClick={() => onSelectProject && onSelectProject(project.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -170,8 +236,16 @@ const Projects: React.FC<ProjectsProps> = ({ onSelectProject }) => {
|
||||
<p>Keine Unternehmen in dieser Kategorie gefunden.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Project Modal */}
|
||||
<CreateProjectModal
|
||||
isOpen={showCreateForm}
|
||||
onClose={() => setShowCreateForm(false)}
|
||||
onCreate={createProject}
|
||||
linkedPlayerName={linkedPlayerName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Projects;
|
||||
export default Projects;
|
||||
|
||||
Reference in New Issue
Block a user