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:
230
components/BannerManagementModal.tsx
Normal file
230
components/BannerManagementModal.tsx
Normal 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">×</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;
|
||||
218
components/CreateProjectModal.tsx
Normal file
218
components/CreateProjectModal.tsx
Normal 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">×</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;
|
||||
131
components/DeleteProjectModal.tsx
Normal file
131
components/DeleteProjectModal.tsx
Normal 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
76
components/EditModal.tsx
Normal 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">×</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;
|
||||
203
components/EmployeeManagementModal.tsx
Normal file
203
components/EmployeeManagementModal.tsx
Normal 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">×</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;
|
||||
205
components/GalleryManagementModal.tsx
Normal file
205
components/GalleryManagementModal.tsx
Normal 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">×</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;
|
||||
@@ -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>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
116
components/MarkdownEditor.tsx
Normal file
116
components/MarkdownEditor.tsx
Normal 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('', '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;
|
||||
392
components/ShopManagementModal.tsx
Normal file
392
components/ShopManagementModal.tsx
Normal 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">×</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;
|
||||
Reference in New Issue
Block a user