'use client'; /** * Mitgliederverwaltung fuer Modals (DB-basiertes Einladungssystem). * * Statt Invite-Links werden bestehende Benutzer direkt per E-Mail * ueber die Datenbank gesucht und dem Kalender zugeordnet: * - Suchfeld mit Live-Vorschlaegen (GET /api/users/search?q=...) * - Hinzufuegen per Klick (POST /api/calendars/[id]/members) * - Mitgliederliste mit Entfernen-Button (DELETE ...?memberId=...) * * Nur fuer Admins gerendert; der Server prueft die Berechtigung final. */ import { useCallback, useEffect, useRef, useState } from 'react'; import { ErrorMessage } from '@/components/ui/ui'; /** Mitglied, wie der Members-Endpoint es liefert. */ interface MemberRow { id: string; createdAt: string; user: { id: string; email: string; username: string | null }; } /** Treffer der Benutzer-Suche. */ interface UserSuggestion { id: string; email: string; username: string | null; } export function MemberSection({ calendarId }: { calendarId: string }) { const [members, setMembers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [query, setQuery] = useState(''); const [suggestions, setSuggestions] = useState([]); const [isSearching, setIsSearching] = useState(false); const [addingEmail, setAddingEmail] = useState(null); const [removingId, setRemovingId] = useState(null); const searchTimer = useRef | null>(null); /** Laedt die aktuelle Mitgliederliste. */ const loadMembers = useCallback(async () => { setIsLoading(true); setError(null); try { const response = await fetch(`/api/calendars/${calendarId}/members`); if (!response.ok) { const data = (await response.json().catch(() => ({}))) as { error?: string }; setError(data.error ?? 'Mitglieder konnten nicht geladen werden.'); return; } const data = (await response.json()) as { members: MemberRow[] }; setMembers(data.members); } catch { setError('Netzwerkfehler. Bitte erneut versuchen.'); } finally { setIsLoading(false); } }, [calendarId]); useEffect(() => { void loadMembers(); }, [loadMembers]); /** Entfernt die Suchvorschlaege, wenn das Suchfeld den Fokus verliert. * Die kleine Verzoegerung stellt sicher, dass Klicks auf Vorschlaege * (die nach dem Blur-Event kommen) noch verarbeitet werden. */ function hideSuggestionsDelayed() { window.setTimeout(() => setSuggestions([]), 150); } /** Fuegt einen Benutzer per E-Mail zum Kalender hinzu. */ async function addMember(email: string) { setAddingEmail(email); setError(null); try { const response = await fetch(`/api/calendars/${calendarId}/members`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); if (!response.ok) { const data = (await response.json().catch(() => ({}))) as { error?: string }; setError(data.error ?? 'Hinzufuegen fehlgeschlagen.'); return; } setQuery(''); setSuggestions([]); await loadMembers(); } catch { setError('Netzwerkfehler. Bitte erneut versuchen.'); } finally { setAddingEmail(null); } } /** Entfernt ein Mitglied aus dem Kalender. */ async function removeMember(memberId: string) { setRemovingId(memberId); setError(null); try { const response = await fetch( `/api/calendars/${calendarId}/members?memberId=${encodeURIComponent(memberId)}`, { method: 'DELETE' }, ); if (!response.ok) { const data = (await response.json().catch(() => ({}))) as { error?: string }; setError(data.error ?? 'Entfernen fehlgeschlagen.'); return; } await loadMembers(); } catch { setError('Netzwerkfehler. Bitte erneut versuchen.'); } finally { setRemovingId(null); } } /** Live-Suche mit 300 ms Debounce (schont die Datenbank). */ function handleQueryChange(nextQuery: string) { setQuery(nextQuery); if (searchTimer.current) { clearTimeout(searchTimer.current); } if (nextQuery.trim().length < 2) { setSuggestions([]); return; } searchTimer.current = setTimeout(async () => { setIsSearching(true); try { const response = await fetch( `/api/users/search?q=${encodeURIComponent(nextQuery.trim())}`, ); if (response.ok) { const data = (await response.json()) as { users: UserSuggestion[] }; setSuggestions(data.users); } } catch { // Suche ist optional; Fehler still ignorieren, Vorschlaege bleiben leer. } finally { setIsSearching(false); } }, 300); } return (

Mitglieder ({members.length})

{/* Suchfeld mit DB-Anbindung (Live-Vorschlaege). */}
handleQueryChange(event.target.value)} onBlur={hideSuggestionsDelayed} placeholder="Benutzer per E-Mail suchen und hinzufuegen …" aria-label="Benutzer suchen" className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:border-slate-500 focus:outline-none focus:ring-1 focus:ring-slate-500 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100 dark:placeholder:text-slate-500" /> {(suggestions.length > 0 || isSearching) && query.trim().length >= 2 && (
    {isSearching && (
  • Suche …
  • )} {suggestions.map((suggestion) => (
  • ))} {!isSearching && suggestions.length === 0 && (
  • Kein Benutzer gefunden.
  • )}
)}
{isLoading ? (

Mitglieder werden geladen …

) : members.length === 0 ? (

Noch keine Mitglieder. Suche oben nach Benutzern, um sie hinzuzufuegen.

) : (
    {members.map((member) => (
  • {member.user.username ?? member.user.email} {member.user.username && ( {member.user.email} )}
  • ))}
)}
); }