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

@@ -0,0 +1,230 @@
import React, { useState, useEffect } from 'react';
import { Icons } from './IconSet';
interface BannerManagementModalProps {
isOpen: boolean;
onClose: () => void;
projectId: string;
currentBannerUrl: string;
onUpdate: () => void;
}
const BannerManagementModal: React.FC<BannerManagementModalProps> = ({
isOpen,
onClose,
projectId,
currentBannerUrl,
onUpdate
}) => {
const [bannerUrl, setBannerUrl] = useState(currentBannerUrl);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [previewLoading, setPreviewLoading] = useState(false);
useEffect(() => {
if (isOpen) {
setBannerUrl(currentBannerUrl);
setError(null);
}
}, [isOpen, currentBannerUrl]);
const updateBanner = async () => {
if (!bannerUrl.trim()) {
setError('Banner-URL ist erforderlich');
return;
}
try {
setLoading(true);
setError(null);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ bannerUrl: bannerUrl.trim() })
});
if (response.ok) {
onUpdate();
onClose();
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Aktualisieren des Banners');
}
} catch (err) {
console.error('Error updating banner:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
const handleImageLoad = () => {
setPreviewLoading(false);
};
const handleImageError = () => {
setPreviewLoading(false);
setError('Bild konnte nicht geladen werden');
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-surface border border-border rounded-xl w-full max-w-2xl shadow-2xl flex flex-col max-h-[90vh]">
<div className="p-4 border-b border-border flex justify-between items-center bg-surfaceHighlight/20">
<h3 className="font-bold text-textMain flex items-center gap-2">
<Icons.Layers className="w-5 h-5" />
Banner bearbeiten
</h3>
<button onClick={onClose} className="text-textMuted hover:text-white transition-colors text-xl leading-none">&times;</button>
</div>
<div className="p-6 flex-1 overflow-y-auto">
{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>
)}
{/* Current Banner Preview */}
<div className="mb-6">
<h4 className="font-semibold text-textMain mb-3">Aktuelles Banner</h4>
<div className="relative h-32 rounded-lg overflow-hidden border border-border bg-surfaceHighlight/30">
{currentBannerUrl ? (
<>
{previewLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-accentInfo"></div>
</div>
)}
<img
src={currentBannerUrl}
alt="Current banner"
className="w-full h-full object-cover"
onLoad={handleImageLoad}
onError={handleImageError}
/>
</>
) : (
<div className="w-full h-full flex items-center justify-center text-textMuted">
<Icons.Layers className="w-8 h-8" />
</div>
)}
</div>
</div>
{/* Banner URL Input */}
<div className="mb-6">
<h4 className="font-semibold text-textMain mb-3">Neue Banner-URL</h4>
<input
type="url"
value={bannerUrl}
onChange={(e) => setBannerUrl(e.target.value)}
placeholder="https://example.com/banner-image.jpg"
className="w-full bg-[#0b0b0d] border border-border rounded p-3 text-sm"
/>
<p className="text-xs text-textMuted mt-2">
Geben Sie eine direkte URL zu einem Bild ein. Empfohlene Größe: 1200x400 Pixel oder größer.
</p>
</div>
{/* Preview */}
{bannerUrl && bannerUrl !== currentBannerUrl && (
<div className="mb-6">
<h4 className="font-semibold text-textMain mb-3">Vorschau</h4>
<div className="relative h-32 rounded-lg overflow-hidden border border-border bg-surfaceHighlight/30">
<img
src={bannerUrl}
alt="Banner preview"
className="w-full h-full object-cover"
onError={() => setError('Vorschau-Bild konnte nicht geladen werden')}
/>
</div>
</div>
)}
{/* Common Banner Suggestions */}
<div className="mb-6">
<h4 className="font-semibold text-textMain mb-3">Beispiele</h4>
<div className="grid grid-cols-2 gap-2 text-xs">
<button
onClick={() => setBannerUrl('https://images.unsplash.com/photo-1449824913935-59a10b8d2000?q=80&w=1200&auto=format&fit=crop')}
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
>
<div className="font-medium">Berglandschaft</div>
<div className="text-textMuted truncate">Unsplash</div>
</button>
<button
onClick={() => setBannerUrl('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?q=80&w=1200&auto=format&fit=crop')}
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
>
<div className="font-medium">Stadt bei Nacht</div>
<div className="text-textMuted truncate">Unsplash</div>
</button>
<button
onClick={() => setBannerUrl('https://images.unsplash.com/photo-1542601906990-b4d3fb778b09?q=80&w=1200&auto=format&fit=crop')}
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
>
<div className="font-medium">Architektur</div>
<div className="text-textMuted truncate">Unsplash</div>
</button>
<button
onClick={() => setBannerUrl('https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=1200&auto=format&fit=crop')}
className="p-2 bg-surfaceHighlight/50 rounded hover:bg-surfaceHighlight transition-colors text-left"
>
<div className="font-medium">Abstrakt</div>
<div className="text-textMuted truncate">Unsplash</div>
</button>
</div>
</div>
{/* Info */}
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
<div className="flex items-start gap-3">
<Icons.Terminal className="w-5 h-5 text-blue-400 mt-0.5" />
<div>
<h4 className="text-sm font-medium text-blue-400 mb-1">Banner-Empfehlungen</h4>
<ul className="text-xs text-blue-300 space-y-1">
<li> Verwenden Sie hochwertige Bilder mit 16:9 Seitenverhältnis</li>
<li> Stellen Sie sicher, dass die Bilder öffentlich zugänglich sind</li>
<li> Dunklere Bilder funktionieren oft besser mit dem Text-Overlay</li>
</ul>
</div>
</div>
</div>
</div>
<div className="p-4 border-t border-border flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors"
>
Abbrechen
</button>
<button
onClick={updateBanner}
disabled={loading || !bannerUrl.trim() || bannerUrl === currentBannerUrl}
className="px-6 py-2 text-sm font-medium bg-orange-500 hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded transition-colors flex items-center gap-2"
>
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Aktualisiere...
</>
) : (
<>
<Icons.Layers className="w-4 h-4" />
Banner aktualisieren
</>
)}
</button>
</div>
</div>
</div>
);
};
export default BannerManagementModal;

View File

@@ -0,0 +1,218 @@
import React, { useState } from 'react';
import { Project } from '../types';
import { Icons } from './IconSet';
interface CreateProjectModalProps {
isOpen: boolean;
onClose: () => void;
onCreate: (projectData: {
title: string;
description: string;
category: Project['category'];
}) => Promise<void>;
linkedPlayerName?: string | null;
}
const CreateProjectModal: React.FC<CreateProjectModalProps> = ({ isOpen, onClose, onCreate, linkedPlayerName }) => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [category, setCategory] = useState<Project['category']>('Enterprise');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const categories = [
{ value: 'Enterprise', label: 'Unternehmen', description: 'Firmen, Läden, Dienstleistungen' },
{ value: 'Story Arc', label: 'Story Arc', description: 'Rollenspiel-Handlung, Quest' },
{ value: 'Faction', label: 'Fraktion', description: 'Gruppe, Gilde, Organisation' },
{ value: 'Black Market', label: 'Schwarzmarkt', description: 'Illegale Geschäfte (Vorsicht!)' },
];
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim() || !description.trim()) {
setError('Titel und Beschreibung sind erforderlich');
return;
}
try {
setLoading(true);
setError(null);
await onCreate({ title: title.trim(), description: description.trim(), category });
onClose();
// Reset form
setTitle('');
setDescription('');
setCategory('Enterprise');
} catch (err) {
console.error('Error creating project:', err);
setError('Fehler beim Erstellen des Projekts');
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-surface border border-border rounded-xl w-full max-w-2xl shadow-2xl flex flex-col max-h-[90vh]">
<div className="p-4 border-b border-border flex justify-between items-center bg-surfaceHighlight/20">
<h3 className="font-bold text-textMain flex items-center gap-2">
<Icons.ShoppingBag className="w-5 h-5" />
Neues Unternehmen erstellen
</h3>
<button onClick={onClose} className="text-textMuted hover:text-white transition-colors text-xl leading-none">&times;</button>
</div>
<form onSubmit={handleSubmit} className="p-6 flex-1 overflow-y-auto">
<div className="space-y-6">
{/* Title */}
<div>
<label className="block text-sm font-medium text-textMain mb-2">
Name des Unternehmens *
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full bg-[#0b0b0d] border border-border rounded-lg p-3 text-sm text-gray-300 focus:border-accentInfo focus:outline-none"
placeholder="z.B. DrKButz Architektur GmbH"
required
maxLength={100}
/>
</div>
{/* Category */}
<div>
<label className="block text-sm font-medium text-textMain mb-3">
Kategorie *
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{categories.map((cat) => (
<label
key={cat.value}
className={`p-3 border rounded-lg cursor-pointer transition-all ${
category === cat.value
? 'border-accentInfo bg-accentInfo/10'
: 'border-border hover:border-accentInfo/50'
}`}
>
<input
type="radio"
name="category"
value={cat.value}
checked={category === cat.value}
onChange={(e) => setCategory(e.target.value as Project['category'])}
className="sr-only"
/>
<div className="flex items-start gap-3">
<div className={`w-4 h-4 rounded-full border-2 mt-0.5 ${
category === cat.value
? 'border-accentInfo bg-accentInfo'
: 'border-textMuted'
}`}>
{category === cat.value && (
<div className="w-full h-full rounded-full bg-white scale-50"></div>
)}
</div>
<div>
<div className="font-medium text-textMain">{cat.label}</div>
<div className="text-xs text-textMuted mt-1">{cat.description}</div>
</div>
</div>
</label>
))}
</div>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-textMain mb-2">
Beschreibung *
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full h-32 bg-[#0b0b0d] border border-border rounded-lg p-3 text-sm text-gray-300 focus:border-accentInfo focus:outline-none resize-none"
placeholder="Beschreiben Sie Ihr Unternehmen, seine Dienstleistungen und Ziele..."
required
maxLength={1000}
/>
<div className="text-xs text-textMuted mt-1">
{description.length}/1000 Zeichen
</div>
</div>
{/* Linked Player Info */}
{linkedPlayerName && (
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4">
<div className="flex items-start gap-3">
<Icons.Users className="w-5 h-5 text-green-400 mt-0.5" />
<div>
<h4 className="text-sm font-medium text-green-400 mb-1">Projekt-Ersteller</h4>
<p className="text-sm text-green-300">
Dieses Unternehmen wird im Namen von <strong>{linkedPlayerName}</strong> erstellt.
</p>
</div>
</div>
</div>
)}
{/* Error */}
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
{/* Info */}
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
<div className="flex items-start gap-3">
<Icons.Crown className="w-5 h-5 text-blue-400 mt-0.5" />
<div>
<h4 className="text-sm font-medium text-blue-400 mb-1">Wichtige Hinweise</h4>
<ul className="text-xs text-blue-300 space-y-1">
<li> Sie sind automatisch der Eigentümer und können das Unternehmen jederzeit bearbeiten</li>
<li> Das Unternehmen startet mit dem Status "aktiv"</li>
<li> Sie können später Mitarbeiter hinzufügen und einen Shop einrichten</li>
</ul>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-3 mt-8 pt-6 border-t border-border">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors"
disabled={loading}
>
Abbrechen
</button>
<button
type="submit"
disabled={loading || !title.trim() || !description.trim()}
className="px-6 py-2 text-sm font-medium bg-accentInfo hover:bg-accentInfo/90 text-white rounded transition-colors shadow-lg shadow-accentInfo/20 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Erstelle...
</>
) : (
<>
<Icons.ShoppingBag className="w-4 h-4" />
Unternehmen erstellen
</>
)}
</button>
</div>
</form>
</div>
</div>
);
};
export default CreateProjectModal;

View File

@@ -0,0 +1,131 @@
import React, { useState } from 'react';
import { Icons } from './IconSet';
interface DeleteProjectModalProps {
isOpen: boolean;
onClose: () => void;
projectId: string;
projectTitle: string;
onDelete: () => Promise<void>;
}
const DeleteProjectModal: React.FC<DeleteProjectModalProps> = ({
isOpen,
onClose,
projectId,
projectTitle,
onDelete
}) => {
const [confirmText, setConfirmText] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const requiredText = `LÖSCHE ${projectTitle}`;
const handleDelete = async () => {
if (confirmText !== requiredText) {
setError('Bitte geben Sie den korrekten Text ein');
return;
}
try {
setLoading(true);
setError(null);
await onDelete();
onClose();
} catch (err) {
console.error('Error deleting project:', err);
setError('Fehler beim Löschen des Projekts');
} finally {
setLoading(false);
}
};
const resetAndClose = () => {
setConfirmText('');
setError(null);
onClose();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-surface border border-red-500/20 rounded-xl w-full max-w-md shadow-2xl">
<div className="p-4 border-b border-red-500/20 bg-red-500/5">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-500/20 rounded-full flex items-center justify-center">
<Icons.Shield className="w-5 h-5 text-red-400" />
</div>
<div>
<h3 className="font-bold text-red-400">Projekt löschen</h3>
<p className="text-sm text-red-300">Diese Aktion kann nicht rückgängig gemacht werden</p>
</div>
</div>
</div>
<div className="p-6">
<div className="mb-6">
<h4 className="font-semibold text-textMain mb-2">
Sind Sie absolut sicher?
</h4>
<p className="text-sm text-textMuted mb-4">
Das Löschen von <strong>"{projectTitle}"</strong> wird alle zugehörigen Daten unwiderruflich entfernen:
</p>
<ul className="text-sm text-textMuted space-y-1 mb-4">
<li> Alle Shop-Artikel und Dienstleistungen</li>
<li> Die komplette Bildergalerie</li>
<li> Alle Mitarbeiter-Zuweisungen</li>
<li> Projekt-Beschreibung und Einstellungen</li>
</ul>
</div>
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 mb-6">
<label className="block text-sm font-medium text-red-400 mb-2">
Geben Sie <strong>{requiredText}</strong> ein, um zu bestätigen:
</label>
<input
type="text"
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
className="w-full bg-[#0b0b0d] border border-red-500/30 rounded p-2 text-sm font-mono"
placeholder={requiredText}
/>
{error && (
<p className="text-red-400 text-xs mt-2">{error}</p>
)}
</div>
<div className="flex gap-3">
<button
onClick={resetAndClose}
disabled={loading}
className="flex-1 px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors border border-border rounded"
>
Abbrechen
</button>
<button
onClick={handleDelete}
disabled={confirmText !== requiredText || loading}
className="flex-1 bg-red-500 hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed text-white px-4 py-2 rounded text-sm font-medium transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Lösche...
</>
) : (
<>
<Icons.Shield className="w-4 h-4" />
Endgültig löschen
</>
)}
</button>
</div>
</div>
</div>
</div>
);
};
export default DeleteProjectModal;

76
components/EditModal.tsx Normal file
View File

@@ -0,0 +1,76 @@
import React, { useState, useEffect } from 'react';
import MarkdownEditor from './MarkdownEditor';
interface EditModalProps {
isOpen: boolean;
title: string;
initialValue: string;
multiline?: boolean;
markdown?: boolean; // New prop for markdown editor
onClose: () => void;
onSave: (value: string) => void;
}
const EditModal: React.FC<EditModalProps> = ({ isOpen, title, initialValue, multiline = false, markdown = false, onClose, onSave }) => {
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-surface border border-border rounded-xl w-full max-w-2xl shadow-2xl flex flex-col max-h-[90vh]">
<div className="p-4 border-b border-border flex justify-between items-center bg-surfaceHighlight/20">
<h3 className="font-bold text-textMain">{title}</h3>
<button onClick={onClose} className="text-textMuted hover:text-white transition-colors text-xl leading-none">&times;</button>
</div>
<div className="p-4 flex-1 overflow-hidden flex flex-col">
{multiline ? (
markdown ? (
<MarkdownEditor
value={value}
onChange={setValue}
className="flex-1"
/>
) : (
<textarea
className="w-full h-64 md:h-96 bg-[#0b0b0d] border border-border rounded-lg p-3 text-sm font-mono text-gray-300 focus:border-accentInfo focus:outline-none resize-none"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)
) : (
<input
type="text"
className="w-full bg-[#0b0b0d] border border-border rounded-lg p-3 text-sm text-gray-300 focus:border-accentInfo focus:outline-none"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)}
{multiline && !markdown && <p className="text-xs text-textMuted mt-2">Unterstützt Markdown Formatierung.</p>}
</div>
<div className="p-4 border-t border-border flex justify-end gap-3 bg-surfaceHighlight/10">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors"
>
Abbrechen
</button>
<button
onClick={() => { onSave(value); onClose(); }}
className="px-4 py-2 text-sm font-medium bg-accentInfo hover:bg-accentInfo/90 text-white rounded transition-colors shadow-lg shadow-accentInfo/20"
>
Speichern
</button>
</div>
</div>
</div>
);
};
export default EditModal;

View File

@@ -0,0 +1,203 @@
import React, { useState, useEffect } from 'react';
import { Icons } from './IconSet';
interface EmployeeManagementModalProps {
isOpen: boolean;
onClose: () => void;
projectId: string;
onUpdate: () => void;
}
const EmployeeManagementModal: React.FC<EmployeeManagementModalProps> = ({
isOpen,
onClose,
projectId,
onUpdate
}) => {
const [employees, setEmployees] = useState<string[]>([]);
const [availablePlayers, setAvailablePlayers] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [selectedPlayer, setSelectedPlayer] = useState('');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (isOpen && projectId) {
loadEmployees();
loadAvailablePlayers();
}
}, [isOpen, projectId]);
const loadEmployees = async () => {
try {
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/employees`);
if (response.ok) {
const data = await response.json();
setEmployees(data);
}
} catch (err) {
console.error('Error loading employees:', err);
}
};
const loadAvailablePlayers = async () => {
try {
const response = await fetch('https://vollidioten.ceraticsoft.de/api/players');
if (response.ok) {
const players = await response.json();
// Filter out players who are already employees
const available = players.filter((player: any) =>
!employees.includes(player.username)
);
setAvailablePlayers(available);
}
} catch (err) {
console.error('Error loading available players:', err);
}
};
const addEmployee = async () => {
if (!selectedPlayer) return;
try {
setLoading(true);
setError(null);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/employees`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ employeeName: selectedPlayer })
});
if (response.ok) {
await loadEmployees();
setSelectedPlayer('');
onUpdate();
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Hinzufügen');
}
} catch (err) {
console.error('Error adding employee:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
const removeEmployee = async (employeeName: string) => {
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/employees/${employeeName}`, {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
await loadEmployees();
onUpdate();
} else {
setError('Fehler beim Entfernen');
}
} catch (err) {
console.error('Error removing employee:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-surface border border-border rounded-xl w-full max-w-2xl shadow-2xl flex flex-col max-h-[90vh]">
<div className="p-4 border-b border-border flex justify-between items-center bg-surfaceHighlight/20">
<h3 className="font-bold text-textMain flex items-center gap-2">
<Icons.Users className="w-5 h-5" />
Mitarbeiter verwalten
</h3>
<button onClick={onClose} className="text-textMuted hover:text-white transition-colors text-xl leading-none">&times;</button>
</div>
<div className="p-6 flex-1 overflow-y-auto">
{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>
)}
{/* Add Employee */}
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4 mb-6">
<h4 className="font-semibold text-textMain mb-4">Mitarbeiter hinzufügen</h4>
<div className="flex gap-2">
<select
value={selectedPlayer}
onChange={(e) => setSelectedPlayer(e.target.value)}
className="flex-1 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="">Spieler auswählen...</option>
{availablePlayers.map((player) => (
<option key={player.uuid} value={player.username}>
{player.username}
</option>
))}
</select>
<button
onClick={addEmployee}
disabled={!selectedPlayer || loading}
className="bg-accentInfo hover:bg-accentInfo/90 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-medium"
>
Hinzufügen
</button>
</div>
</div>
{/* Current Employees */}
<div className="mb-4">
<h4 className="font-semibold text-textMain mb-4">
Aktuelle Mitarbeiter ({employees.length})
</h4>
{employees.length === 0 ? (
<div className="text-center py-8 text-textMuted">
<p>Noch keine Mitarbeiter hinzugefügt.</p>
</div>
) : (
<div className="space-y-3">
{employees.map((employee) => (
<div key={employee} className="flex items-center justify-between bg-surfaceHighlight/30 border border-border rounded-lg p-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-gray-700 to-gray-900 rounded flex items-center justify-center text-xs font-bold">
{employee.charAt(0).toUpperCase()}
</div>
<span className="font-medium text-textMain">{employee}</span>
</div>
<button
onClick={() => removeEmployee(employee)}
disabled={loading}
className="text-red-400 hover:text-red-300 text-sm px-2 py-1 rounded hover:bg-red-500/10 transition-colors"
>
Entfernen
</button>
</div>
))}
</div>
)}
</div>
</div>
<div className="p-4 border-t border-border flex justify-end">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors"
>
Schließen
</button>
</div>
</div>
</div>
);
};
export default EmployeeManagementModal;

View File

@@ -0,0 +1,205 @@
import React, { useState, useEffect } from 'react';
import { Icons } from './IconSet';
interface GalleryManagementModalProps {
isOpen: boolean;
onClose: () => void;
projectId: string;
onUpdate: () => void;
}
const GalleryManagementModal: React.FC<GalleryManagementModalProps> = ({
isOpen,
onClose,
projectId,
onUpdate
}) => {
const [images, setImages] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [imageUrl, setImageUrl] = useState('');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (isOpen && projectId) {
loadGallery();
}
}, [isOpen, projectId]);
const loadGallery = async () => {
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/gallery`);
if (response.ok) {
const data = await response.json();
setImages(data);
} else {
setError('Fehler beim Laden der Galerie');
}
} catch (err) {
console.error('Error loading gallery:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
const addImage = async () => {
if (!imageUrl.trim()) return;
try {
setLoading(true);
setError(null);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/gallery`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ imageUrl: imageUrl.trim() })
});
if (response.ok) {
await loadGallery();
setImageUrl('');
onUpdate();
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Hinzufügen');
}
} catch (err) {
console.error('Error adding image:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
const removeImage = async (index: number) => {
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/gallery/${index}`, {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
await loadGallery();
onUpdate();
} else {
setError('Fehler beim Löschen');
}
} catch (err) {
console.error('Error removing image:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-surface border border-border rounded-xl w-full max-w-4xl shadow-2xl flex flex-col max-h-[90vh]">
<div className="p-4 border-b border-border flex justify-between items-center bg-surfaceHighlight/20">
<h3 className="font-bold text-textMain flex items-center gap-2">
<Icons.Layers className="w-5 h-5" />
Galerie verwalten
</h3>
<button onClick={onClose} className="text-textMuted hover:text-white transition-colors text-xl leading-none">&times;</button>
</div>
<div className="p-6 flex-1 overflow-y-auto">
{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>
)}
{/* Add Image */}
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4 mb-6">
<h4 className="font-semibold text-textMain mb-4">Bild hinzufügen</h4>
<div className="flex gap-2">
<input
type="url"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
placeholder="Bild-URL eingeben..."
className="flex-1 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
/>
<button
onClick={addImage}
disabled={!imageUrl.trim() || loading}
className="bg-accentInfo hover:bg-accentInfo/90 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-medium"
>
Hinzufügen
</button>
</div>
<p className="text-xs text-textMuted mt-2">
Geben Sie eine direkte URL zu einem Bild ein (z.B. von Imgur, Discord, etc.)
</p>
</div>
{/* Gallery Grid */}
<div className="mb-4">
<h4 className="font-semibold text-textMain mb-4">
Galerie-Bilder ({images.length})
</h4>
{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>
) : images.length === 0 ? (
<div className="text-center py-8 text-textMuted">
<p>Noch keine Bilder in der Galerie.</p>
<p className="text-sm mt-2">Fügen Sie Bild-URLs hinzu, um Ihre Galerie zu füllen.</p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{images.map((imageUrl, index) => (
<div key={index} className="relative group">
<div className="aspect-square rounded-lg overflow-hidden border border-border bg-surfaceHighlight/30">
<img
src={imageUrl}
alt={`Galerie ${index + 1}`}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = 'https://via.placeholder.com/200x200/374151/6b7280?text=Bild+fehlerhaft';
}}
/>
</div>
{/* Delete Button */}
<button
onClick={() => removeImage(index)}
disabled={loading}
className="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-opacity"
title="Bild entfernen"
>
×
</button>
{/* Overlay on hover */}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors rounded-lg pointer-events-none" />
</div>
))}
</div>
)}
</div>
</div>
<div className="p-4 border-t border-border flex justify-end">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors"
>
Schließen
</button>
</div>
</div>
</div>
);
};
export default GalleryManagementModal;

View File

@@ -43,6 +43,9 @@ export const Icons = {
),
Hammer: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="m15 12-8.5 8.5c-.83.83-2.17.83-3 0 0 0 0 0 0 0a2.12 2.12 0 0 1 0-3L12 9"/><path d="M17.64 15 22 10.64"/><path d="m20.91 11.7-1.25-1.25c-.6-.6-.93-1.4-.93-2.25V7.86c0-.55-.45-1-1-1H16.4c-.84 0-1.65-.33-2.25-.93L12.9 4.68c-.6-.6-1.4-.93-2.25-.93H4.86c-.55 0-1 .45-1 1v1.36c0 .84.33 1.65.93 2.25L12 15.64"/></svg>
),
Edit: ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
)
};

View File

@@ -1,5 +1,7 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Icons } from './IconSet';
import { authService } from '../services/AuthService';
import { DiscordUser } from '../types';
interface LayoutProps {
children: React.ReactNode;
@@ -30,6 +32,13 @@ const NavItem = ({
const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [user, setUser] = useState<DiscordUser | null>(null);
useEffect(() => {
// Subscribe to auth changes
const unsubscribe = authService.subscribe(setUser);
return unsubscribe;
}, []);
return (
<div className="min-h-screen flex flex-col font-sans">
@@ -55,6 +64,9 @@ const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
<NavItem active={activeTab === 'players'} label="Bürger" onClick={() => onNavigate('players')} />
{/* <NavItem active={activeTab === 'organizations'} label="Organisationen" onClick={() => onNavigate('organizations')} />*/}
<NavItem active={activeTab === 'projects'} label="Unternehmen" onClick={() => onNavigate('projects')} />
{user?.isAdmin && (
<NavItem active={activeTab === 'admin'} label="Admin" onClick={() => onNavigate('admin')} />
)}
</nav>
</div>
@@ -92,8 +104,11 @@ const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
<div onClick={() => { onNavigate('players'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Bürger</div>
<div onClick={() => { onNavigate('organizations'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Organisationen</div>
<div onClick={() => { onNavigate('projects'); setMobileMenuOpen(false); }} className="block py-2 text-textMuted hover:text-textMain">Unternehmen</div>
{user?.isAdmin && (
<div onClick={() => { onNavigate('admin'); setMobileMenuOpen(false); }} className="block py-2 text-red-400 hover:text-red-300">Admin</div>
)}
<div onClick={() => { onNavigate('datapack'); setMobileMenuOpen(false); }} className="block py-2 text-textMain">Datapack holen</div>
<div onClick={() => { onNavigate('setup'); setMobileMenuOpen(false); }} className="block py-2 text-accentInfo font-mono text-sm border-t border-white/5 pt-4">Admin Setup >_</div>
<div onClick={() => { onNavigate('setup'); setMobileMenuOpen(false); }} className="block py-2 text-accentInfo font-mono text-sm border-t border-white/5 pt-4">{"Admin Setup >_"}</div>
</div>
)}
</header>
@@ -112,10 +127,43 @@ const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
<div className="w-4 h-4 bg-textMuted rounded-full"></div>
<p>© 2024 Obsidian Platform</p>
</div>
<div className="flex gap-8">
<span className="cursor-pointer hover:text-textMain transition-colors">Dokumentation</span>
<span className="cursor-pointer hover:text-textMain transition-colors">Server Status</span>
<span className="cursor-pointer hover:text-textMain transition-colors">Datenschutz</span>
<div className="flex flex-col md:flex-row items-center gap-8">
{/* Auth Section */}
<div className="flex items-center gap-4 mb-4 md:mb-0">
{user ? (
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<img
src={user.avatarUrl}
alt={user.username}
className="w-6 h-6 rounded-full"
/>
<span className="text-textMain font-medium">{user.username}</span>
</div>
<button
onClick={() => authService.logout()}
className="text-xs text-textMuted hover:text-accentInfo transition-colors"
>
Logout
</button>
</div>
) : (
<button
onClick={() => authService.login()}
className="flex items-center gap-2 text-textMain hover:text-accentInfo transition-colors font-medium"
>
<Icons.Users className="w-4 h-4" />
<span>Discord Login</span>
</button>
)}
</div>
{/* Links */}
<div className="flex gap-8">
<span className="cursor-pointer hover:text-textMain transition-colors">Dokumentation</span>
<span className="cursor-pointer hover:text-textMain transition-colors">Server Status</span>
<span className="cursor-pointer hover:text-textMain transition-colors">Datenschutz</span>
</div>
</div>
</div>
</footer>
@@ -123,4 +171,4 @@ const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
);
};
export default Layout;
export default Layout;

View File

@@ -0,0 +1,116 @@
import React, { useState, useRef } from 'react';
import { Icons } from './IconSet';
interface MarkdownEditorProps {
value: string;
onChange: (value: string) => void;
className?: string;
}
const MarkdownEditor: React.FC<MarkdownEditorProps> = ({ value, onChange, className = '' }) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const insertText = (before: string, after: string = '', placeholder: string = 'text') => {
const textarea = textareaRef.current;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = value.substring(start, end);
const textToInsert = selectedText || placeholder;
const newText = value.substring(0, start) + before + textToInsert + after + value.substring(end);
onChange(newText);
// Set cursor position after the inserted text
setTimeout(() => {
const newCursorPos = start + before.length + textToInsert.length + after.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
textarea.focus();
}, 0);
};
const formatButtons = [
{
icon: 'H',
label: 'Überschrift',
action: () => insertText('# ', ''),
className: 'text-lg font-bold'
},
{
icon: <Icons.Hammer className="w-3 h-3" />,
label: 'Fett',
action: () => insertText('**', '**', 'fetter Text'),
},
{
icon: <Icons.Tag className="w-3 h-3" />,
label: 'Kursiv',
action: () => insertText('*', '*', 'kursiver Text'),
},
{
icon: <Icons.Scroll className="w-3 h-3" />,
label: 'Liste',
action: () => insertText('- ', ''),
},
{
icon: <Icons.Crown className="w-3 h-3" />,
label: 'Nummerierte Liste',
action: () => insertText('1. ', ''),
},
{
icon: <Icons.Shield className="w-3 h-3" />,
label: 'Zitat',
action: () => insertText('> ', ''),
},
{
icon: '🔗',
label: 'Link',
action: () => insertText('[', '](url)', 'Link-Text'),
},
{
icon: '📷',
label: 'Bild',
action: () => insertText('![', '](url)', 'Alt-Text'),
},
];
return (
<div className={`border border-border rounded-lg overflow-hidden ${className}`}>
{/* Toolbar */}
<div className="bg-surfaceHighlight border-b border-border p-2 flex flex-wrap gap-1">
{formatButtons.map((button, index) => (
<button
key={index}
type="button"
onClick={button.action}
className="p-1.5 hover:bg-white/10 rounded text-textMuted hover:text-textMain transition-colors text-sm flex items-center justify-center min-w-[32px] h-8"
title={button.label}
>
{typeof button.icon === 'string' ? (
<span className={button.className}>{button.icon}</span>
) : (
button.icon
)}
</button>
))}
</div>
{/* Textarea */}
<textarea
ref={textareaRef}
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full h-64 md:h-96 bg-[#0b0b0d] border-0 p-4 text-sm font-mono text-gray-300 focus:outline-none resize-none"
placeholder="Schreibe dein Journal hier... Verwende die Toolbar für Formatierung."
spellCheck={false}
/>
{/* Footer */}
<div className="bg-surfaceHighlight border-t border-border px-4 py-2 text-xs text-textMuted">
Markdown-Formatierung unterstützt. Verwende die Toolbar für schnelle Formatierung.
</div>
</div>
);
};
export default MarkdownEditor;

View File

@@ -0,0 +1,392 @@
import React, { useState, useEffect } from 'react';
import { Icons } from './IconSet';
interface ShopItem {
id: string;
name: string;
description: string;
price: number;
currency: string;
stock: number;
type: 'item' | 'service';
materialsRequired?: string;
}
interface ShopManagementModalProps {
isOpen: boolean;
onClose: () => void;
projectId: string;
onUpdate: () => void;
}
const ShopManagementModal: React.FC<ShopManagementModalProps> = ({
isOpen,
onClose,
projectId,
onUpdate
}) => {
const [items, setItems] = useState<ShopItem[]>([]);
const [loading, setLoading] = useState(false);
const [showAddForm, setShowAddForm] = useState(false);
const [editingItem, setEditingItem] = useState<ShopItem | null>(null);
const [error, setError] = useState<string | null>(null);
// Form state for add/edit
const [formData, setFormData] = useState({
name: '',
description: '',
price: '',
currency: 'Gold',
stock: '',
type: 'item' as 'item' | 'service',
materialsRequired: ''
});
useEffect(() => {
if (isOpen && projectId) {
loadShopItems();
}
}, [isOpen, projectId]);
const loadShopItems = async () => {
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop`);
if (response.ok) {
const data = await response.json();
setItems(data);
} else {
setError('Fehler beim Laden der Shop-Artikel');
}
} catch (err) {
console.error('Error loading shop items:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
const resetForm = () => {
setFormData({
name: '',
description: '',
price: '',
currency: 'Gold',
stock: '',
type: 'item',
materialsRequired: ''
});
setEditingItem(null);
setShowAddForm(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim() || !formData.description.trim() || !formData.price) {
setError('Name, Beschreibung und Preis sind erforderlich');
return;
}
try {
setLoading(true);
setError(null);
const itemData = {
name: formData.name.trim(),
description: formData.description.trim(),
price: parseFloat(formData.price),
currency: formData.currency,
stock: parseInt(formData.stock) || 0,
type: formData.type,
materialsRequired: formData.materialsRequired.trim() || undefined
};
let response;
if (editingItem) {
// Update existing item
response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop/${editingItem.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(itemData)
});
} else {
// Add new item
response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(itemData)
});
}
if (response.ok) {
await loadShopItems();
resetForm();
onUpdate();
} else {
const errorData = await response.json();
setError(errorData.error || 'Fehler beim Speichern');
}
} catch (err) {
console.error('Error saving shop item:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
const handleEdit = (item: ShopItem) => {
setFormData({
name: item.name,
description: item.description,
price: item.price.toString(),
currency: item.currency,
stock: item.stock.toString(),
type: item.type,
materialsRequired: item.materialsRequired || ''
});
setEditingItem(item);
setShowAddForm(true);
};
const handleDelete = async (itemId: string) => {
if (!confirm('Artikel wirklich löschen?')) return;
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/shop/${itemId}`, {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
await loadShopItems();
onUpdate();
} else {
setError('Fehler beim Löschen');
}
} catch (err) {
console.error('Error deleting shop item:', err);
setError('Netzwerkfehler');
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-surface border border-border rounded-xl w-full max-w-4xl shadow-2xl flex flex-col max-h-[90vh]">
<div className="p-4 border-b border-border flex justify-between items-center bg-surfaceHighlight/20">
<h3 className="font-bold text-textMain flex items-center gap-2">
<Icons.ShoppingBag className="w-5 h-5" />
Shop verwalten
</h3>
<button onClick={onClose} className="text-textMuted hover:text-white transition-colors text-xl leading-none">&times;</button>
</div>
<div className="p-6 flex-1 overflow-y-auto">
{/* Add/Edit Form */}
{showAddForm && (
<div className="bg-surfaceHighlight/30 border border-border rounded-lg p-4 mb-6">
<h4 className="font-semibold text-textMain mb-4">
{editingItem ? 'Artikel bearbeiten' : 'Neuen Artikel hinzufügen'}
</h4>
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Name *</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
required
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Beschreibung *</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
className="w-full h-20 bg-[#0b0b0d] border border-border rounded p-2 text-sm"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Preis *</label>
<input
type="number"
step="0.01"
value={formData.price}
onChange={(e) => setFormData({...formData, price: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Währung</label>
<select
value={formData.currency}
onChange={(e) => setFormData({...formData, currency: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="Gold">Gold</option>
<option value="Diamonds">Diamanten</option>
<option value="Credits">Credits</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Bestand</label>
<input
type="number"
value={formData.stock}
onChange={(e) => setFormData({...formData, stock: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-textMain mb-1">Typ</label>
<select
value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value as 'item' | 'service'})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
>
<option value="item">Produkt</option>
<option value="service">Dienstleistung</option>
</select>
</div>
{formData.type === 'service' && (
<div className="md:col-span-2">
<label className="block text-sm font-medium text-textMain mb-1">Materialanforderungen</label>
<input
type="text"
value={formData.materialsRequired}
onChange={(e) => setFormData({...formData, materialsRequired: e.target.value})}
className="w-full bg-[#0b0b0d] border border-border rounded p-2 text-sm"
placeholder="z.B. Kunde stellt Steinziegel"
/>
</div>
)}
<div className="md:col-span-2 flex gap-2 pt-2">
<button
type="submit"
disabled={loading}
className="bg-accentInfo hover:bg-accentInfo/90 text-white px-4 py-2 rounded text-sm font-medium disabled:opacity-50"
>
{loading ? 'Speichere...' : (editingItem ? 'Aktualisieren' : 'Hinzufügen')}
</button>
<button
type="button"
onClick={resetForm}
className="bg-surfaceHighlight hover:bg-white/10 px-4 py-2 rounded text-sm"
>
Abbrechen
</button>
</div>
</form>
</div>
)}
{/* Error Display */}
{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>
)}
{/* Header with Add Button */}
<div className="flex justify-between items-center mb-6">
<h4 className="text-lg font-semibold text-textMain">Artikel ({items.length})</h4>
<button
onClick={() => setShowAddForm(true)}
className="bg-accentInfo hover:bg-accentInfo/90 text-white px-4 py-2 rounded text-sm font-medium flex items-center gap-2"
>
<Icons.ShoppingBag className="w-4 h-4" />
Artikel hinzufügen
</button>
</div>
{/* Items List */}
{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>
) : items.length === 0 ? (
<div className="text-center py-8 text-textMuted">
<p>Noch keine Artikel im Shop.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{items.map((item) => (
<div key={item.id} className="bg-surfaceHighlight/30 border border-border rounded-lg p-4">
<div className="flex justify-between items-start mb-2">
<h5 className="font-medium text-textMain">{item.name}</h5>
<span className={`text-xs px-2 py-1 rounded ${
item.type === 'service' ? 'bg-amber-500/20 text-amber-400' : 'bg-blue-500/20 text-blue-400'
}`}>
{item.type === 'service' ? 'Dienst' : 'Produkt'}
</span>
</div>
<p className="text-sm text-textMuted mb-3 line-clamp-2">{item.description}</p>
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-medium text-accentInfo">
{item.price} {item.currency}
</div>
<div className="text-xs text-textMuted">
Bestand: {item.stock}
</div>
</div>
{item.materialsRequired && (
<div className="text-xs text-amber-400 bg-amber-500/10 p-2 rounded mb-3">
Material: {item.materialsRequired}
</div>
)}
<div className="flex gap-2">
<button
onClick={() => handleEdit(item)}
className="flex-1 bg-accentInfo hover:bg-accentInfo/90 text-white px-3 py-1 rounded text-xs font-medium"
>
Bearbeiten
</button>
<button
onClick={() => handleDelete(item.id)}
className="flex-1 bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-xs font-medium"
>
Löschen
</button>
</div>
</div>
))}
</div>
)}
</div>
<div className="p-4 border-t border-border flex justify-end">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-textMuted hover:text-white transition-colors"
>
Schließen
</button>
</div>
</div>
</div>
);
};
export default ShopManagementModal;