Files
project_vollidioten_website/components/NpcGalleryManagementModal.tsx

272 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { Icons } from './IconSet';
interface GalleryImage {
id: string;
url: string;
}
interface NpcGalleryManagementModalProps {
isOpen: boolean;
onClose: () => void;
projectId: string;
onUpdate: () => void;
}
const NpcGalleryManagementModal: React.FC<NpcGalleryManagementModalProps> = ({
isOpen,
onClose,
projectId,
onUpdate
}) => {
const [images, setImages] = useState<GalleryImage[]>([]);
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 (imageId: string) => {
try {
setLoading(true);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/projects/${projectId}/gallery/${imageId}`, {
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" />
NPC-Portfolio 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>
{/* URL Input */}
<div className="flex gap-2 mb-4">
<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 mb-4">
Geben Sie eine direkte URL zu einem Bild ein (z.B. von Imgur, Discord, etc.)
</p>
{/* File Upload */}
<div className="border-t border-border pt-4">
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center hover:border-accentInfo/50 transition-colors">
<input
type="file"
accept="image/*"
multiple
onChange={async (e) => {
const files = e.target.files as FileList;
if (!files || files.length === 0) return;
try {
setLoading(true);
setError(null);
// Upload each file
const fileArray = Array.from(files) as File[];
for (const file of fileArray) {
const formData = new FormData();
formData.append('gallery', file);
const response = await fetch(`https://vollidioten.ceraticsoft.de/api/admin/npc-companies/${projectId}/gallery/upload`, {
method: 'POST',
credentials: 'include',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
setError(errorData.error || `Fehler beim Hochladen von ${file.name}`);
break;
}
}
// Reload gallery after all uploads
await loadGallery();
} catch (err) {
console.error('Error uploading images:', err);
setError('Netzwerkfehler beim Hochladen');
} finally {
setLoading(false);
}
}}
className="hidden"
id="npc-gallery-upload"
/>
<label htmlFor="npc-gallery-upload" className="cursor-pointer">
<div className="w-10 h-10 mx-auto mb-3 bg-surfaceHighlight rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-accentInfo" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</div>
<p className="text-sm text-textMain font-medium">Bilder auswählen</p>
<p className="text-xs text-textMuted mt-1">Max. 10MB pro Bild, Mehrfachauswahl möglich</p>
</label>
</div>
</div>
</div>
{/* Gallery Grid */}
<div className="mb-4">
<h4 className="font-semibold text-textMain mb-4">
Portfolio-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 im Portfolio.</p>
<p className="text-sm mt-2">Fügen Sie Bilder hinzu, um das Portfolio Ihrer NPC-Firma zu füllen.</p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{images.map((image, index) => (
<div key={image.id} className="relative group">
<div className="aspect-square rounded-lg overflow-hidden border border-border bg-surfaceHighlight/30">
<img
src={image.url}
alt={`Portfolio ${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(image.id)}
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 NpcGalleryManagementModal;