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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user