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

201
pages/LinkPlayer.tsx Normal file
View 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;