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:
Lars Behrends
2025-12-28 16:46:04 +01:00
parent 6abdffe22a
commit d3d7ec46e6
40 changed files with 5967 additions and 102 deletions

View File

@@ -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;