// /src/app/[locale]/components/TeamMemberView.tsx 'use client' import { useEffect, useMemo, useRef, useState } from 'react' import { DndContext, closestCenter, DragOverlay, type DragStartEvent, type DragEndEvent, } from '@dnd-kit/core' import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' import { DroppableZone } from './DroppableZone' import MiniCard from './MiniCard' import MiniCardDummy from './MiniCardDummy' import SortableMiniCard from './SortableMiniCard' import LeaveTeamModal from './LeaveTeamModal' import InvitePlayersModal from './InvitePlayersModal' import Modal from './Modal' import type { Player, InvitedPlayer, Team, TeamJoinPolicy } from '@/types/team' import { AnimatePresence, motion } from 'framer-motion' import { leaveTeam, reloadTeam, renameTeam } from '@/lib/sse-actions' import Button from './Button' import NextImage from 'next/image' import TeamPremierRankBadge from './TeamPremierRankBadge' import Link from 'next/link' import { useTeamStore } from '@/lib/stores' import { useSSEStore } from '@/lib/useSSEStore' import { TEAM_EVENTS, SELF_EVENTS, isSseEventType, type SSEEventType, } from '@/lib/sseEvents' type Props = { team?: Team activeDragItem: Player | null isDragging: boolean showLeaveModal: boolean showInviteModal: boolean currentUserSteamId: string setShowLeaveModal: (v: boolean) => void setShowInviteModal: (v: boolean) => void setActiveDragItem: (item: Player | null) => void setIsDragging: (v: boolean) => void adminMode?: boolean } export default function TeamMemberView(props: Props) { const { team: storeTeam, setTeam } = useTeamStore() // Prop -> Store spiegeln useEffect(() => { if (!props.team) return const curr = useTeamStore.getState().team if (!curr || curr.id !== props.team.id) { setTeam(props.team as Team) return } // gleiche ID → selektiv patchen const next = props.team as Team const diff: Partial = {} if (curr.name !== next.name) diff.name = next.name if (curr.logo !== next.logo) diff.logo = next.logo if ((curr.leader?.steamId ?? null) !== (next.leader?.steamId ?? null)) diff.leader = next.leader if (typeof next.joinPolicy === 'string' && curr.joinPolicy !== next.joinPolicy) { diff.joinPolicy = next.joinPolicy as TeamJoinPolicy } if (Object.keys(diff).length) setTeam({ ...curr, ...diff } as Team) }, [props.team, setTeam]) if (!props.adminMode && !props.currentUserSteamId) return null const team = props.team ?? storeTeam ?? null if (!team) return null return } function TeamMemberViewBody({ team, activeDragItem, isDragging, showLeaveModal, showInviteModal, currentUserSteamId, setShowLeaveModal, setShowInviteModal, setActiveDragItem, setIsDragging, adminMode = false, }: Props & { team: Team }) { const { setTeam } = useTeamStore() const teamId = team.id const teamLeaderSteamId = team.leader?.steamId ?? '' // stabile Menge für useEffect-Deps const RELEVANT = useMemo>( () => new Set([...TEAM_EVENTS, ...SELF_EVENTS]), [] ) const isLeader = currentUserSteamId === team.leader?.steamId const canManage = adminMode || isLeader const canInvite = isLeader && !adminMode const canAddDirect = adminMode const isDraggingRef = useRef(false) const [pendingRemote, setPendingRemote] = useState<{ active: Player[] inactive: Player[] invited: InvitedPlayer[] } | null>(null) const [remountKey, setRemountKey] = useState(0) const { connect, lastEvent, isConnected } = useSSEStore() const [activePlayers, setActivePlayers] = useState([]) const [inactivePlayers, setInactivePlayers] = useState([]) const [invitedPlayers, setInvitedPlayers] = useState([]) const [kickCandidate, setKickCandidate] = useState(null) const [promoteCandidate, setPromoteCandidate] = useState(null) const [showDeleteModal, setShowDeleteModal] = useState(false) const [isEditingName, setIsEditingName] = useState(false) const [editedName, setEditedName] = useState(team.name || '') const [saveSuccess, setSaveSuccess] = useState(false) const [joinPolicy, setJoinPolicy] = useState( (team.joinPolicy as TeamJoinPolicy) ?? 'REQUEST' ) const policyChangedAtRef = useRef(null) const [savingPolicy, setSavingPolicy] = useState(false) const [policySaved, setPolicySaved] = useState(false) const [inviteKey, setInviteKey] = useState(0) const openInvite = () => { setInviteKey(k => k + 1) setShowInviteModal(true) } // Cache-Busting fürs Logo (ohne any) type TeamWithStamp = Team & { logoUpdatedAt?: string | Date; updatedAt?: string | Date } const tStamped = team as TeamWithStamp const initialLogoVersion = tStamped.logoUpdatedAt ? new Date(tStamped.logoUpdatedAt).getTime() : tStamped.updatedAt ? new Date(tStamped.updatedAt).getTime() : 0 const [logoVersion, setLogoVersion] = useState(initialLogoVersion) // Upload-Progress const [isUploadingLogo, setIsUploadingLogo] = useState(false) const [uploadPct, setUploadPct] = useState(0) const R = 28, S = 64, CIRC = 2 * Math.PI * R const dashOffset = CIRC - (uploadPct / 100) * CIRC const fileInputRef = useRef(null) const isClickable = canManage && !isUploadingLogo // SSE-Verbindung useEffect(() => { if (!currentUserSteamId) return if (!isConnected) connect(currentUserSteamId) }, [currentUserSteamId, connect, isConnected]) const eqByIds = (a: Player[], b: Player[]) => { if (a.length !== b.length) return false const aa = a.map(p=>p.steamId).join(',') const bb = b.map(p=>p.steamId).join(',') return aa === bb } const eqSetByIds = (a: {steamId:string}[], b: {steamId:string}[]) => { if (a.length !== b.length) return false const sa = [...a.map(p => p.steamId)].sort() const sb = [...b.map(p => p.steamId)].sort() for (let i = 0; i < sa.length; i++) { if (sa[i] !== sb[i]) return false } return true } useEffect(() => { if (typeof team.joinPolicy === 'string') { setJoinPolicy(team.joinPolicy as TeamJoinPolicy) } }, [team.id, team.joinPolicy]) // Team-Listen lokal synchronisieren useEffect(() => { const nextActive = (team.activePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name)) const nextInactive = (team.inactivePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name)) const nextInvited = Array.from(new Map((team.invitedPlayers ?? []).map(p => [p.steamId, p])).values()) .sort((a,b)=>a.name.localeCompare(b.name)) const unchanged = eqByIds(activePlayers, nextActive) && eqByIds(inactivePlayers, nextInactive) && eqByIds(invitedPlayers, nextInvited) if (unchanged) return if (!isDraggingRef.current) { setActivePlayers(nextActive) setInactivePlayers(nextInactive) setInvitedPlayers(nextInvited) } else { setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [team?.id, team?.name, team?.logo, team?.leader, team?.activePlayers, team?.inactivePlayers, team?.invitedPlayers]) // Relevante SSE-Events useEffect(() => { if (!lastEvent || !team.id) return if (!isSseEventType(lastEvent.type)) return const payload = (lastEvent.payload ?? {}) as Record const now = Date.now() // Nach lokalem Speichern: kurzes Ignore-Fenster if (lastEvent.type === 'team-updated' && payload.teamId === team.id) { if (policyChangedAtRef.current && (now - policyChangedAtRef.current) < 2000) { policyChangedAtRef.current = null return } } // nur Logo geändert → minimal patchen if (lastEvent.type === 'team-logo-updated') { const curr = useTeamStore.getState().team const filename = (payload as { filename?: string }).filename const version = (payload as { version?: number }).version const evTeamId = (payload as { teamId?: string }).teamId if (evTeamId && evTeamId !== team.id) return if (filename && curr) setTeam({ ...curr, logo: filename }) if (typeof version === 'number') setLogoVersion(version) return } // Rest: reload wenn relevant if (!RELEVANT.has(lastEvent.type)) return const evTeamId = (payload as { teamId?: string }).teamId if (evTeamId && evTeamId !== team.id) return ;(async () => { const updated = await reloadTeam(team.id) if (!updated) return setTeam(updated) setEditedName(updated.name || '') if (typeof updated.joinPolicy === 'string') { setJoinPolicy(updated.joinPolicy as TeamJoinPolicy) } const nextActive = (updated.activePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name)) const nextInactive = (updated.inactivePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name)) const nextInvited = Array.from(new Map((updated.invitedPlayers ?? []).map(p => [p.steamId, p])).values()) .sort((a,b)=>a.name.localeCompare(b.name)) if (isDraggingRef.current) { setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited }) return } const contentChanged = !eqSetByIds(activePlayers, nextActive) || !eqSetByIds(inactivePlayers, nextInactive) || !eqSetByIds(invitedPlayers, nextInvited) const orderChanged = !eqByIds(activePlayers, nextActive) || !eqByIds(inactivePlayers, nextInactive) || !eqByIds(invitedPlayers, nextInvited) if (contentChanged) { setActivePlayers(nextActive) setInactivePlayers(nextInactive) setInvitedPlayers(nextInvited) setRemountKey(k => k + 1) } else if (orderChanged) { setActivePlayers(nextActive) setInactivePlayers(nextInactive) setInvitedPlayers(nextInvited) } })() }, [RELEVANT, lastEvent, team.id, setTeam, activePlayers, inactivePlayers, invitedPlayers]) const handleDragStart = (event: DragStartEvent) => { const id = String(event.active.id) const item = activePlayers.find(p => p.steamId === id) || inactivePlayers.find(p => p.steamId === id) if (item) { setActiveDragItem(item) setIsDragging(true) isDraggingRef.current = true } } const [showPolicyMenu, setShowPolicyMenu] = useState(false) const policyMenuRef = useRef(null) const applyPolicy = async (p: TeamJoinPolicy) => { if (p === joinPolicy) { setShowPolicyMenu(false); return } setJoinPolicy(p) await saveJoinPolicy(p) setShowPolicyMenu(false) } useEffect(() => { if (!showPolicyMenu) return const onOutside = (e: PointerEvent) => { if (!policyMenuRef.current) return if (!policyMenuRef.current.contains(e.target as Node)) { e.preventDefault() e.stopPropagation() setShowPolicyMenu(false) } } const onEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') setShowPolicyMenu(false) } document.addEventListener('pointerdown', onOutside, { capture: true }) document.addEventListener('keydown', onEsc) return () => { document.removeEventListener('pointerdown', onOutside, { capture: true }) document.removeEventListener('keydown', onEsc) } }, [showPolicyMenu]) const updateTeamMembers = async (tId: string, active: Player[], inactive: Player[]) => { try { const res = await fetch('/api/team/update-players', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ teamId: tId, activePlayers: active.map(p => p.steamId), inactivePlayers: inactive.map(p => p.steamId), }), }) if (!res.ok) throw new Error('Update fehlgeschlagen') const updated = await reloadTeam(tId) if (updated) setTeam(updated) } catch (err) { console.error('Fehler beim Aktualisieren:', err) } } const handleDragEnd = async (event: DragEndEvent) => { setActiveDragItem(null) setIsDragging(false) isDraggingRef.current = false const { active, over } = event if (!over) { if (pendingRemote) { setActivePlayers(pendingRemote.active) setInactivePlayers(pendingRemote.inactive) setInvitedPlayers(pendingRemote.invited) setPendingRemote(null) } return } const activeId = String(active.id) const overId = String(over.id) const movingItem = activePlayers.find(p => p.steamId === activeId) || inactivePlayers.find(p => p.steamId === activeId) if (!movingItem) return const wasInActive = activePlayers.some(p => p.steamId === activeId) const dropToActive = overId === 'active' || activePlayers.some(p => p.steamId === overId) if ((wasInActive && dropToActive) || (!wasInActive && !dropToActive)) { if (pendingRemote) { setActivePlayers(pendingRemote.active) setInactivePlayers(pendingRemote.inactive) setInvitedPlayers(pendingRemote.invited) setPendingRemote(null) } return } let nextActive = [...activePlayers] let nextInactive = [...inactivePlayers] if (dropToActive) { if (nextActive.length >= 5) return nextInactive = nextInactive.filter(p => p.steamId !== activeId) if (!nextActive.some(p => p.steamId === activeId)) nextActive.push(movingItem) } else { nextActive = nextActive.filter(p => p.steamId !== activeId) if (!nextInactive.some(p => p.steamId === activeId)) nextInactive.push(movingItem) } nextActive.sort((a,b)=>a.name.localeCompare(b.name)) nextInactive.sort((a,b)=>a.name.localeCompare(b.name)) const noChange = eqByIds(nextActive, activePlayers) && eqByIds(nextInactive, inactivePlayers) if (noChange) { if (pendingRemote) { setActivePlayers(pendingRemote.active) setInactivePlayers(pendingRemote.inactive) setInvitedPlayers(pendingRemote.invited) setPendingRemote(null) } return } setActivePlayers(nextActive) setInactivePlayers(nextInactive) updateTeamMembers(teamId, nextActive, nextInactive).catch(console.error) setSaveSuccess(true) setTimeout(()=>setSaveSuccess(false), 3000) if (pendingRemote) { const diff = !eqByIds(pendingRemote.active, nextActive) || !eqByIds(pendingRemote.inactive, nextInactive) || !eqByIds(pendingRemote.invited, invitedPlayers) if (diff) { setActivePlayers(pendingRemote.active) setInactivePlayers(pendingRemote.inactive) setInvitedPlayers(pendingRemote.invited) } setPendingRemote(null) } } const handleReload = async () => { const updated = await reloadTeam(team.id) if (updated) setTeam(updated) } const confirmKick = async () => { if (!kickCandidate) return const newActive = activePlayers.filter(p => p.steamId !== kickCandidate.steamId) const newInactive = inactivePlayers.filter(p => p.steamId !== kickCandidate.steamId) setActivePlayers(newActive) setInactivePlayers(newInactive) await fetch('/api/team/kick', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ steamId: kickCandidate.steamId, teamId }), }) await updateTeamMembers(team.id, newActive, newInactive) setKickCandidate(null) } const promoteToLeader = async (newLeaderId: string) => { try { const res = await fetch('/api/team/transfer-leader', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ teamId, newLeaderSteamId: newLeaderId }), }) if (!res.ok) { const data: unknown = await res.json().catch(() => ({})) console.error('Fehler bei Leader-Übertragung:', (data as { message?: string }).message) return } await handleReload() } catch (err) { console.error('Fehler bei Leader-Übertragung:', err) } } type DownscaleOpts = { size?: number quality?: number mime?: string square?: boolean } async function saveJoinPolicy(next: TeamJoinPolicy = joinPolicy) { const prev = joinPolicy try { setSavingPolicy(true) const res = await fetch('/api/team/update-join-policy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', cache: 'no-store', body: JSON.stringify({ teamId, joinPolicy: next }), }) if (!res.ok) { const data: unknown = await res.json().catch(() => ({})) const msg = (data as { message?: string }).message throw new Error(msg ?? `Speichern fehlgeschlagen (${res.status})`) } const parsed: unknown = await res.json().catch(() => ({})) const serverPolicy = (parsed as { joinPolicy?: TeamJoinPolicy }).joinPolicy const patched = (serverPolicy ?? next) as TeamJoinPolicy setJoinPolicy(patched) const curr = useTeamStore.getState().team if (curr && curr.id === teamId && curr.joinPolicy !== patched) { setTeam({ ...curr, joinPolicy: patched }) } policyChangedAtRef.current = Date.now() setPolicySaved(true) setTimeout(() => setPolicySaved(false), 2000) } catch (e) { setJoinPolicy(prev) console.error(e) alert((e as Error).message || 'Speichern fehlgeschlagen') } finally { setSavingPolicy(false) } } async function canEncode(mime: string): Promise { try { if ('OffscreenCanvas' in window) { const c = new OffscreenCanvas(2, 2) const b = await c.convertToBlob({ type: mime, quality: 0.8 }) return !!b } const c = document.createElement('canvas') c.width = 2; c.height = 2 const url = c.toDataURL(mime) return typeof url === 'string' && url.startsWith(`data:${mime}`) } catch { return false } } async function downscaleImage(file: File, opts: DownscaleOpts = {}): Promise { const { size = 512, quality = 0.85, mime: wantedMime = 'image/webp', square = true, } = opts // 1) Bild laden (ImageBitmap bevorzugt, ohne any) let url: string | null = null let img: ImageBitmap | HTMLImageElement try { img = await createImageBitmap(file) } catch { url = URL.createObjectURL(file) img = await new Promise((res, rej) => { const im = new window.Image() im.onload = () => res(im) im.onerror = rej im.src = url! }) } const dims = img as unknown as { width: number; height: number } const srcW = dims.width const srcH = dims.height if (!srcW || !srcH) { if (url) URL.revokeObjectURL(url) if ('close' in (img as ImageBitmap)) try { (img as ImageBitmap).close() } catch {} throw new Error('Invalid image dimensions') } // 2) Zielgröße + optionaler Center-Crop let sx = 0, sy = 0, sw = srcW, sh = srcH if (square) { const side = Math.min(srcW, srcH) sx = Math.max(0, Math.floor((srcW - side) / 2)) sy = Math.max(0, Math.floor((srcH - side) / 2)) sw = side; sh = side } const scale = Math.min(size / sw, size / sh, 1) const dw = Math.max(1, Math.round(sw * scale)) const dh = Math.max(1, Math.round(sh * scale)) // 3) Canvas (Offscreen bevorzugt) const source = img as unknown as CanvasImageSource const offscreen = 'OffscreenCanvas' in window let blob: Blob | null = null if (offscreen) { const c = new OffscreenCanvas(dw, dh) const ctx = c.getContext('2d', { alpha: true })! ctx.imageSmoothingQuality = 'high' ctx.drawImage(source, sx, sy, sw, sh, 0, 0, dw, dh) // 4) Format mit Fallbacks const canWebp = await canEncode('image/webp') const canJpeg = await canEncode('image/jpeg') const targetMime = (wantedMime === 'image/webp' && canWebp) ? 'image/webp' : (wantedMime === 'image/jpeg' && canJpeg) ? 'image/jpeg' : canWebp ? 'image/webp' : canJpeg ? 'image/jpeg' : 'image/png' blob = await c.convertToBlob({ type: targetMime, quality: targetMime === 'image/png' ? undefined : quality }) } else { const c = document.createElement('canvas') c.width = dw; c.height = dh const ctx = c.getContext('2d')! ctx.imageSmoothingQuality = 'high' ctx.drawImage(source, sx, sy, sw, sh, 0, 0, dw, dh) const canWebp = await canEncode('image/webp') const canJpeg = await canEncode('image/jpeg') const targetMime = (wantedMime === 'image/webp' && canWebp) ? 'image/webp' : (wantedMime === 'image/jpeg' && canJpeg) ? 'image/jpeg' : canWebp ? 'image/webp' : canJpeg ? 'image/jpeg' : 'image/png' blob = await new Promise((res) => c.toBlob(b => res(b), targetMime, targetMime === 'image/png' ? undefined : quality) ) } if (url) URL.revokeObjectURL(url) if ('close' in (img as ImageBitmap)) { try { (img as ImageBitmap).close() } catch {} } if (!blob) throw new Error('Canvas encoding failed (toBlob returned null)') return blob } async function uploadTeamLogo(file: File) { return new Promise((resolve, reject) => { const formData = new FormData() formData.append('logo', file) formData.append('teamId', teamId) const xhr = new XMLHttpRequest() xhr.open('POST', '/api/team/upload-logo') xhr.upload.onprogress = (e) => { if (e.lengthComputable) setUploadPct(Math.round((e.loaded / e.total) * 100)) else setUploadPct(p => (p < 90 ? p + 1 : p)) } xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { const json: unknown = JSON.parse(xhr.responseText) const current = useTeamStore.getState().team const filename = (json as { filename?: string }).filename const version = (json as { version?: number }).version if (filename && current) setTeam({ ...current, logo: filename }) if (typeof version === 'number') setLogoVersion(version) } catch {} resolve() } else { reject(new Error('Upload fehlgeschlagen')) } } xhr.onerror = () => reject(new Error('Netzwerkfehler beim Upload')) setIsUploadingLogo(true) setUploadPct(0) xhr.send(formData) }) } const manageSteam: string = adminMode ? teamLeaderSteamId : currentUserSteamId const renderMemberList = (players: Player[]) => ( {players.map(player => ( { if (isDragging) e.preventDefault() }} > setPromoteCandidate(player)} currentUserSteamId={manageSteam} teamLeaderSteamId={teamLeaderSteamId} isAdmin={adminMode} isDraggingGlobal={isDragging} hideOverlay={isDragging} matchParentBg /> ))} ) return (
{ if (isClickable) fileInputRef.current?.click() }} onKeyDown={(e) => { if (!isClickable) return if (e.key === "Enter" || e.key === " ") { e.preventDefault() fileInputRef.current?.click() } }} title={isUploadingLogo ? "Upload läuft…" : (canManage ? "Logo hochladen" : undefined)} > {canManage && isClickable && (
)} {isUploadingLogo && (
{uploadPct}%
)}
{canManage && ( { if (isUploadingLogo) return const file = e.target.files?.[0] if (!file) return try { const blob = await downscaleImage(file, { size: 512, quality: 0.85, mime: 'image/webp', square: true }) const mime = blob.type || 'image/webp' const ext = mime === 'image/jpeg' ? 'jpg' : mime === 'image/png' ? 'png' : 'webp' const processed = new File([blob], `${team!.id}.${ext}`, { type: mime }) await uploadTeamLogo(processed) } catch (err) { console.error('Fehler beim Hochladen des Logos:', err) alert('Fehler beim Hochladen des Logos.') } finally { setTimeout(() => { setIsUploadingLogo(false); setUploadPct(0) }, 300) e.currentTarget.value = '' } }} /> )}
{isEditingName ? ( <> setEditedName(e.target.value)} className="py-1.5 px-3 border rounded-lg text-sm dark:bg-neutral-800 dark:border-neutral-700 dark:text-white" /> ) : ( <>

{team.name ?? 'Team'}

{canManage && ( <> {/* Policy-Pill */}
{showPolicyMenu && (
e.stopPropagation()} onClick={(e) => e.stopPropagation()} >
)}
)} )}
{canManage && ( )}
p.steamId).join(',')}`} items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}> {renderMemberList(activePlayers)} p.steamId).join(',')}`} items={inactivePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}> {renderMemberList(inactivePlayers)} {canManage && (
)}
{invitedPlayers.length > 0 && (

Eingeladene Spieler

{invitedPlayers.map((player) => ( {}} draggable={false} currentUserSteamId={currentUserSteamId} teamLeaderSteamId={team.leader?.steamId} isSelectable={false} isInvite={true} rank={player.premierRank} // optional lokales Extra-Feld sicher lesen invitationId={(player as InvitedPlayer & { invitationId?: string }).invitationId} onKick={async (sid) => { setInvitedPlayers(list => list.filter(p => p.steamId !== sid)) try { await fetch('/api/user/invitations/revoke', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ invitationId: (player as InvitedPlayer & { invitationId?: string }).invitationId ?? undefined, teamId: team.id, steamId: sid, }), }) } catch (e) { console.error('Revoke fehlgeschlagen:', e) setInvitedPlayers(list => [...list, player].sort((a,b)=>a.name.localeCompare(b.name))) } finally { const updated = await reloadTeam(team.id) if (updated) setTeam(updated) } }} /> ))}
)}
{activeDragItem && ( )}
{canInvite && ( setShowInviteModal(false)} onSuccess={() => {}} team={team} /> )} {canAddDirect && ( setShowInviteModal(false)} onSuccess={() => setShowInviteModal(false)} team={team} directAdd /> )} {isLeader && ( setShowLeaveModal(false)} onSuccess={() => setShowLeaveModal(false)} team={team} /> )} {canManage && promoteCandidate && ( setPromoteCandidate(null)} onSave={async () => { await promoteToLeader(promoteCandidate.steamId) setPromoteCandidate(null) }} closeButtonTitle="Übertragen" closeButtonColor="blue" >
{}} draggable={false} rank={promoteCandidate.premierRank} currentUserSteamId={currentUserSteamId} teamLeaderSteamId={team.leader?.steamId} hideActions isSelectable={false} />

Möchtest du {promoteCandidate.name} wirklich zum Team-Leader machen?

)} {canManage && kickCandidate && ( setKickCandidate(null)} onSave={confirmKick} closeButtonTitle="Entfernen" closeButtonColor="red" >
{}} draggable={false} rank={kickCandidate.premierRank} currentUserSteamId={currentUserSteamId} teamLeaderSteamId={team.leader?.steamId} hideActions isSelectable={false} />

Möchtest du {kickCandidate.name} wirklich aus dem Team entfernen?

)} {canManage && ( setShowDeleteModal(false)} onSave={async () => { await fetch('/api/team/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ teamId: team.id }), }) setShowDeleteModal(false) window.location.href = '/team' }} closeButtonTitle="Team löschen" closeButtonColor="red" >

Bist du sicher, dass du dieses Team löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.

)}
) }