Files
2026-09-14 16:55:29 +02:00

348 lines
20 KiB
TypeScript

import React, { useState } from 'react';
import { Expert } from '../types';
import { supabase, TABLES } from '../lib/supabase';
interface ExpertsProps {
experts: Expert[];
setExperts: React.Dispatch<React.SetStateAction<Expert[]>>;
refreshData: () => Promise<void>;
}
export const Experts: React.FC<ExpertsProps> = ({ experts, refreshData }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingExpert, setEditingExpert] = useState<Expert | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [expertToDeleteId, setExpertToDeleteId] = useState<string | null>(null);
const [tagInput, setTagInput] = useState('');
const [formState, setFormState] = useState({
name: '',
role: '',
email: '',
phone: '',
linkedin: '',
status: 'Verfügbar' as Expert['status'],
avatar: '',
bio: '',
tags: [] as string[]
});
const filteredExperts = experts.filter(e =>
e.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
e.role.toLowerCase().includes(searchTerm.toLowerCase()) ||
e.tags?.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
);
const openEditModal = (expert: Expert) => {
setEditingExpert(expert);
setFormState({
name: expert.name,
role: expert.role,
email: expert.email || '',
phone: expert.phone || '',
linkedin: expert.linkedin || '',
status: expert.status,
avatar: expert.avatar || '',
bio: expert.bio || '',
tags: expert.tags || []
});
setTagInput('');
setIsModalOpen(true);
};
const openCreateModal = () => {
setEditingExpert(null);
setFormState({
name: '',
role: '',
email: '',
phone: '',
linkedin: '',
status: 'Verfügbar',
avatar: '',
bio: '',
tags: []
});
setTagInput('');
setIsModalOpen(true);
};
const handleAddTag = (e?: React.FormEvent) => {
if (e) e.preventDefault();
const tag = tagInput.trim();
if (tag && !formState.tags.includes(tag)) {
setFormState({ ...formState, tags: [...formState.tags, tag] });
setTagInput('');
}
};
const removeTag = (tagToRemove: string) => {
setFormState({ ...formState, tags: formState.tags.filter(t => t !== tagToRemove) });
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (editingExpert) {
const { error } = await supabase
.from(TABLES.EXPERTS)
.update(formState)
.eq('id', editingExpert.id);
if (error) console.error(error);
} else {
const { error } = await supabase.from(TABLES.EXPERTS).insert([formState]);
if (error) console.error(error);
}
setIsModalOpen(false);
refreshData();
};
const confirmDeleteExpert = async () => {
if (!expertToDeleteId) return;
try {
await supabase.from(TABLES.EXPERTS).delete().eq('id', expertToDeleteId);
await refreshData();
} catch (error) {
console.error('Delete error:', error);
} finally {
setExpertToDeleteId(null);
}
};
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setFormState({ ...formState, avatar: reader.result as string });
};
reader.readAsDataURL(file);
}
};
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row gap-4 justify-between items-start md:items-center mb-8">
<div className="relative flex-1 w-full max-w-xl">
<input
type="text"
placeholder="Namen, Rollen oder Tags suchen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-12 pr-6 py-4 bg-white dark:bg-slate-900 border-none rounded-2xl shadow-sm focus:ring-2 focus:ring-brand-500 transition-all text-lg dark:text-white"
/>
<svg className="w-6 h-6 absolute left-4 top-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
</div>
<button
onClick={openCreateModal}
className="bg-brand-600 hover:bg-brand-700 text-white px-6 py-4 rounded-2xl font-bold transition-all shadow-lg shadow-brand-500/20 flex items-center gap-2 whitespace-nowrap w-full md:w-auto justify-center"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
Experte hinzufügen
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{filteredExperts.map((expert) => (
<div key={expert.id} className="bg-white dark:bg-slate-900 rounded-[2.5rem] p-6 border border-slate-100 dark:border-slate-800 hover:shadow-2xl transition-all group flex flex-col relative overflow-hidden h-full">
<div className="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => setExpertToDeleteId(expert.id)}
className="p-2 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm text-slate-400 hover:text-red-500 rounded-full shadow-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</div>
<div className="flex flex-col items-center text-center mb-6">
<div className="relative mb-4">
{expert.avatar ? (
<img src={expert.avatar} alt={expert.name} className="w-24 h-24 rounded-[2rem] object-cover shadow-xl ring-4 ring-slate-50 dark:ring-slate-800" />
) : (
<div className="w-24 h-24 rounded-[2rem] bg-brand-500/10 text-brand-600 flex items-center justify-center font-bold text-3xl uppercase ring-4 ring-slate-50 dark:ring-slate-800">
{expert.name.charAt(0)}
</div>
)}
<div className={`absolute -bottom-1 -right-1 w-6 h-6 rounded-full border-4 border-white dark:border-slate-900 ${
expert.status === 'Verfügbar' ? 'bg-brand-500' :
expert.status === 'Gebucht' ? 'bg-blue-500' : 'bg-slate-400'
}`}></div>
</div>
<h4 className="font-bold text-xl leading-tight text-slate-900 dark:text-white group-hover:text-brand-500 transition-colors">{expert.name}</h4>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">{expert.role}</p>
</div>
<div className="flex-1 mb-4 flex flex-col">
<div className="p-4 bg-slate-50 dark:bg-slate-800/40 rounded-2xl border border-slate-100 dark:border-slate-800/50 min-h-[80px] mb-4">
<p className="text-slate-600 dark:text-slate-400 text-xs leading-relaxed line-clamp-3 italic">
{expert.bio || "Keine zusätzlichen Informationen hinterlegt."}
</p>
</div>
{/* Tags Display moved below bio Box */}
{expert.tags && expert.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 justify-center mb-2">
{expert.tags.slice(0, 4).map(tag => (
<span key={tag} className="px-2.5 py-1 bg-brand-50 dark:bg-brand-500/10 text-[9px] font-extrabold text-brand-700 dark:text-brand-400 rounded-lg uppercase tracking-wider border border-brand-100 dark:border-brand-500/20 shadow-sm">
{tag}
</span>
))}
{expert.tags.length > 4 && (
<span className="text-[9px] font-bold text-slate-400 self-center">+{expert.tags.length - 4}</span>
)}
</div>
)}
</div>
<div className="space-y-2 mb-6 text-[13px]">
<div className="flex items-center gap-3 text-slate-500">
<svg className="w-4 h-4 opacity-40 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
<span className="truncate">{expert.email || 'n/a'}</span>
</div>
<div className="flex items-center gap-3 text-slate-500">
<svg className="w-4 h-4 opacity-40 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /></svg>
<span className="truncate">{expert.phone || 'n/a'}</span>
</div>
</div>
<button
onClick={() => openEditModal(expert)}
className="w-full py-3.5 bg-slate-900 dark:bg-slate-800 text-white dark:text-slate-200 font-bold rounded-2xl hover:bg-brand-600 dark:hover:bg-brand-600 transition-all text-xs uppercase tracking-widest shadow-lg shadow-slate-900/10"
>
Profil bearbeiten
</button>
</div>
))}
{filteredExperts.length === 0 && (
<div className="col-span-full py-24 text-center bg-white dark:bg-slate-900 rounded-[3rem] border-2 border-dashed border-slate-200 dark:border-slate-800">
<div className="w-20 h-20 bg-slate-50 dark:bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-6 text-slate-300">
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
</div>
<p className="text-slate-500 font-medium mb-4">Keine Experten gefunden.</p>
<button onClick={openCreateModal} className="text-brand-600 font-bold hover:underline px-6 py-2 bg-brand-50 dark:bg-brand-500/10 rounded-xl transition-all">Neu anlegen</button>
</div>
)}
</div>
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-md">
<div className="bg-white dark:bg-slate-900 w-full max-w-2xl rounded-[2.5rem] shadow-2xl p-10 max-h-[90vh] overflow-y-auto border border-slate-100 dark:border-slate-800">
<div className="flex justify-between items-center mb-8">
<h3 className="text-3xl font-bold dark:text-white">{editingExpert ? 'Profil bearbeiten' : 'Neuer Experte'}</h3>
<button onClick={() => setIsModalOpen(false)} className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-white">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2 flex flex-col items-center gap-4 bg-slate-50 dark:bg-slate-800/50 p-6 rounded-3xl border border-dashed border-slate-200 dark:border-slate-700">
<div className="relative group">
{formState.avatar ? (
<img src={formState.avatar} alt="Preview" className="w-32 h-32 rounded-[2rem] object-cover shadow-xl" />
) : (
<div className="w-32 h-32 rounded-[2rem] bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-400">
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</div>
)}
<label className="absolute inset-0 flex items-center justify-center bg-black/40 text-white rounded-[2rem] opacity-0 group-hover:opacity-100 cursor-pointer transition-opacity font-bold text-xs uppercase">
Ändern
<input type="file" className="hidden" accept="image/*" onChange={handleImageUpload} />
</label>
</div>
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-widest">Foto hochladen</p>
</div>
<div>
<label className="block text-xs font-bold uppercase text-slate-400 mb-2 tracking-widest">Name</label>
<input required type="text" value={formState.name} onChange={e => setFormState({...formState, name: e.target.value})} className="w-full px-5 py-4 bg-slate-50 dark:bg-slate-800 border-none rounded-2xl outline-none focus:ring-2 focus:ring-brand-500 transition-all dark:text-white" />
</div>
<div>
<label className="block text-xs font-bold uppercase text-slate-400 mb-2 tracking-widest">Rolle</label>
<input required type="text" value={formState.role} onChange={e => setFormState({...formState, role: e.target.value})} className="w-full px-5 py-4 bg-slate-50 dark:bg-slate-800 border-none rounded-2xl outline-none focus:ring-2 focus:ring-brand-500 transition-all dark:text-white" />
</div>
<div>
<label className="block text-xs font-bold uppercase text-slate-400 mb-2 tracking-widest">E-Mail</label>
<input type="email" value={formState.email} onChange={e => setFormState({...formState, email: e.target.value})} className="w-full px-5 py-4 bg-slate-50 dark:bg-slate-800 border-none rounded-2xl outline-none focus:ring-2 focus:ring-brand-500 transition-all dark:text-white" />
</div>
<div>
<label className="block text-xs font-bold uppercase text-slate-400 mb-2 tracking-widest">Telefon</label>
<input type="text" value={formState.phone} onChange={e => setFormState({...formState, phone: e.target.value})} className="w-full px-5 py-4 bg-slate-50 dark:bg-slate-800 border-none rounded-2xl outline-none focus:ring-2 focus:ring-brand-500 transition-all dark:text-white" />
</div>
{/* Tags Input Section */}
<div className="md:col-span-2 pt-4">
<label className="block text-xs font-bold uppercase text-slate-400 mb-3 tracking-widest">Themenbereiche & Expertisen (Tags)</label>
<div className="flex flex-wrap gap-2 mb-3">
{formState.tags.map(tag => (
<span key={tag} className="flex items-center gap-2 px-3 py-1.5 bg-brand-500 text-white rounded-xl text-xs font-bold shadow-md shadow-brand-500/10">
{tag}
<button type="button" onClick={() => removeTag(tag)} className="hover:text-slate-200 transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</span>
))}
</div>
<div className="flex gap-2">
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleAddTag(e); }}
placeholder="Tag eingeben (z.B. Cloud Computing)..."
className="flex-1 px-5 py-4 bg-slate-50 dark:bg-slate-800 border-none rounded-2xl outline-none focus:ring-2 focus:ring-brand-500 transition-all dark:text-white text-sm"
/>
<button
type="button"
onClick={handleAddTag}
className="px-6 bg-brand-500 text-white rounded-2xl font-bold shadow-lg shadow-brand-500/20 hover:bg-brand-600 transition-all"
>
Hinzufügen
</button>
</div>
</div>
<div className="md:col-span-2">
<label className="block text-xs font-bold uppercase text-slate-400 mb-2 tracking-widest">Kurz-Bio / Informationen</label>
<textarea
value={formState.bio}
onChange={e => setFormState({...formState, bio: e.target.value})}
rows={4}
placeholder="Wichtige Skills, Projekterfahrung oder Hintergrund..."
className="w-full px-5 py-4 bg-slate-50 dark:bg-slate-800 border-none rounded-2xl outline-none focus:ring-2 focus:ring-brand-500 transition-all dark:text-white resize-none"
/>
</div>
</div>
<div className="flex gap-4 pt-4">
<button type="button" onClick={() => setIsModalOpen(false)} className="flex-1 py-4 bg-slate-100 dark:bg-slate-800 text-slate-500 font-bold rounded-2xl hover:bg-slate-200 transition-all">Abbrechen</button>
<button type="submit" className="flex-[2] py-4 bg-brand-600 text-white font-bold rounded-2xl shadow-xl shadow-brand-500/20 hover:bg-brand-700 transition-all">Speichern</button>
</div>
</form>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{expertToDeleteId && (
<div className="fixed inset-0 z-[120] flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-md">
<div className="bg-white dark:bg-slate-900 w-full max-sm rounded-[2rem] p-8 text-center shadow-2xl border border-red-100 dark:border-red-900/30">
<div className="w-16 h-16 bg-red-100 dark:bg-red-900/20 text-red-600 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
</div>
<h3 className="text-2xl font-bold mb-2 dark:text-white">Experten löschen?</h3>
<p className="text-slate-500 mb-8 text-sm">Möchten Sie diesen Experten wirklich unwiderruflich aus der Datenbank löschen?</p>
<div className="grid grid-cols-2 gap-4">
<button onClick={() => setExpertToDeleteId(null)} className="py-4 bg-slate-100 dark:bg-slate-800 rounded-2xl font-bold text-slate-600 dark:text-slate-300">Abbruch</button>
<button onClick={confirmDeleteExpert} className="py-4 bg-red-600 text-white rounded-2xl font-bold shadow-lg shadow-red-500/20 hover:bg-red-700 transition-all">Löschen</button>
</div>
</div>
</div>
)}
</div>
);
};