ironie-nextjs/src/app/components/TeamMemberView.tsx
2025-08-12 12:46:40 +02:00

819 lines
31 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.

'use client'
import { useEffect, useRef, useState } from 'react'
import { DndContext, closestCenter, DragOverlay } 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 { Player } from '../types/team'
import { AnimatePresence, motion } from 'framer-motion'
import { leaveTeam, reloadTeam, renameTeam } from '@/app/lib/sse-actions'
import Button from './Button'
import Image from 'next/image'
import TeamPremierRankBadge from './TeamPremierRankBadge'
import Link from 'next/link'
import { Team } from '../types/team'
import { useTeamStore } from '../lib/stores'
import { useSSEStore } from '@/app/lib/useSSEStore'
import {
TEAM_EVENTS,
SELF_EVENTS,
isSseEventType,
type SSEEventType,
} from '@/app/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
}
type InvitedPlayer = Player & { invitationId?: string }
export default function TeamMemberView({
team: teamProp,
activeDragItem,
isDragging,
showLeaveModal,
showInviteModal,
currentUserSteamId,
setShowLeaveModal,
setShowInviteModal,
setActiveDragItem,
setIsDragging,
adminMode = false,
}: Props) {
const { team: storeTeam, setTeam } = useTeamStore()
const team = teamProp ?? storeTeam
if (!team) return null
const RELEVANT: ReadonlySet<SSEEventType> = new Set([...TEAM_EVENTS, ...SELF_EVENTS])
const isLeader = currentUserSteamId === team.leader
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<Player[]>([])
const [inactivePlayers, setInactivePlayers] = useState<Player[]>([])
const [invitedPlayers, setInvitedPlayers] = useState<InvitedPlayer[]>([])
const [kickCandidate, setKickCandidate] = useState<Player | null>(null)
const [promoteCandidate, setPromoteCandidate] = useState<Player | null>(null)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [isEditingName, setIsEditingName] = useState(false)
const [editedName, setEditedName] = useState(team.name || '')
const [saveSuccess, setSaveSuccess] = useState(false)
// Cache-Busting fürs Logo
const [logoVersion, setLogoVersion] = useState<number | null>(null)
// 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<HTMLInputElement>(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
}
// Team-Listen lokal synchronisieren
useEffect(() => {
if (!team) return
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 ?? {}
// ► Spezialfall: nur Logo aktualisieren (ohne komplettes Reload)
if (lastEvent.type === 'team-logo-updated') {
if (payload.teamId && payload.teamId !== team.id) return
const current = useTeamStore.getState().team
if (payload?.filename && current) {
setTeam({ ...current, logo: payload.filename })
}
if (payload?.version) setLogoVersion(payload.version)
return
}
// andere Team/Self-Events
if (!RELEVANT.has(lastEvent.type)) return
if (payload.teamId && payload.teamId !== team.id) return
;(async () => {
const updated = await reloadTeam(team.id)
if (!updated) return
setTeam(updated)
setEditedName(updated.name || '')
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
}
setActivePlayers(nextActive)
setInactivePlayers(nextInactive)
setInvitedPlayers(nextInvited)
setRemountKey(k => k + 1)
})()
}, [lastEvent, team?.id, setTeam])
const handleDragStart = (event: any) => {
const id = event.active.id as string
const item =
activePlayers.find(p => p.steamId === id) ||
inactivePlayers.find(p => p.steamId === id)
if (item) {
setActiveDragItem(item)
setIsDragging(true)
isDraggingRef.current = true
}
}
const updateTeamMembers = async (teamId: 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,
activePlayers: active.map(p => p.steamId),
inactivePlayers: inactive.map(p => p.steamId),
}),
})
if (!res.ok) throw new Error('Update fehlgeschlagen')
const updated = await reloadTeam(teamId)
if (updated) setTeam(updated)
} catch (err) {
console.error('Fehler beim Aktualisieren:', err)
}
}
const handleDragEnd = async (event: any) => {
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(team.id, 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: team.id }),
})
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: team.id, newLeaderSteamId: newLeaderId }),
})
if (!res.ok) {
const data = await res.json()
console.error('Fehler bei Leader-Übertragung:', data.message)
return
}
await handleReload()
} catch (err) {
console.error('Fehler bei Leader-Übertragung:', err)
}
}
// Upload mit Progress via XHR setzt filename/version direkt, kein Reload nötig
async function uploadTeamLogo(file: File) {
return new Promise<void>((resolve, reject) => {
const formData = new FormData()
formData.append('logo', file)
formData.append('teamId', team!.id)
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 = JSON.parse(xhr.responseText)
const current = useTeamStore.getState().team
if (json?.filename && current) setTeam({ ...current, logo: json.filename })
if (json?.version) setLogoVersion(json.version)
} catch {}
resolve()
} else {
reject(new Error('Upload fehlgeschlagen'))
}
}
xhr.onerror = () => reject(new Error('Netzwerkfehler beim Upload'))
setIsUploadingLogo(true)
setUploadPct(0)
xhr.send(formData)
})
}
if (!adminMode && !currentUserSteamId) return null
const manageSteam: string = adminMode ? (team.leader ?? '') : currentUserSteamId
const renderMemberList = (players: Player[]) => (
<AnimatePresence>
{players.map(player => (
<motion.div
key={player.steamId}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className='max-w-[160px]'
>
<Link
href={`/profile/${player.steamId}`}
passHref
onClick={e => { if (isDragging) e.preventDefault() }}
>
<SortableMiniCard
player={player}
onKick={setKickCandidate}
onPromote={() => setPromoteCandidate(player)}
currentUserSteamId={manageSteam}
teamLeaderSteamId={team.leader}
isAdmin={adminMode}
isDraggingGlobal={isDragging}
hideOverlay={isDragging}
matchParentBg
/>
</Link>
</motion.div>
))}
</AnimatePresence>
)
return (
<div className={`p-4 mt-6 bg-white border border-gray-200 shadow-2xs rounded-xl dark:bg-neutral-800 dark:border-neutral-700 ${isDragging ? 'cursor-grabbing' : ''}`}>
<div className="flex justify-between items-center mb-6 flex-wrap gap-2">
<div className="flex items-center gap-4">
<div className="relative group">
<div
role="button"
aria-disabled={!isClickable}
aria-busy={isUploadingLogo}
tabIndex={isClickable ? 0 : -1}
className={[
"relative w-16 h-16 rounded-full overflow-hidden border border-gray-300 dark:border-neutral-600",
isClickable ? "cursor-pointer" : "cursor-not-allowed opacity-70"
].join(" ")}
onClick={() => { 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)}
>
<Image
key={`${team.logo ?? 'fallback'}-${logoVersion ?? 0}`}
src={
team.logo
? `/assets/img/logos/${team.logo}${logoVersion ? `?v=${logoVersion}` : ''}`
: `/assets/img/logos/cs2.webp`
}
alt="Teamlogo"
fill
sizes="64px"
quality={75}
className={`object-cover ${isUploadingLogo ? 'opacity-70' : ''}`}
priority={false}
/>
{/* Hover-Overlay nur, wenn klickbar */}
{canManage && isClickable && (
<div className="absolute inset-0 bg-black/50 text-white flex flex-col items-center justify-center text-xs font-medium opacity-0 group-hover:opacity-100 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 mb-1" viewBox="0 0 576 512" fill="currentColor">
<path d="M288 109.3L288 352c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-242.7-73.4 73.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l128-128c12.5-12.5 32.8-12.5 45.3 0l128 128c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L288 109.3zM64 352l128 0c0 35.3 28.7 64 64 64s64-28.7 64-64l128 0c35.3 0 64 28.7 64 64l0 32c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64l0-32c0-35.3 28.7-64 64-64zM432 456a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/>
</svg>
</div>
)}
{/* Progress-Kreis (Start bei 12 Uhr via rotate(-90 …)) */}
{isUploadingLogo && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<svg width={S} height={S} viewBox={`0 0 ${S} ${S}`} className="absolute">
<g transform={`rotate(-90 ${S/2} ${S/2})`}>
<circle cx={S/2} cy={S/2} r={R} stroke="rgba(255,255,255,0.35)" strokeWidth="6" fill="none" />
<circle
cx={S/2} cy={S/2} r={R}
stroke="#16a34a"
strokeWidth="6"
fill="none"
strokeLinecap="round"
strokeDasharray={CIRC}
strokeDashoffset={dashOffset}
style={{ transition: 'stroke-dashoffset 120ms linear' }}
/>
</g>
</svg>
<span className="text-[11px] font-semibold text-white drop-shadow">{uploadPct}%</span>
</div>
)}
</div>
{canManage && (
<input
ref={fileInputRef}
type="file"
accept="image/png,image/jpeg,image/webp"
id="logoUpload"
className="hidden"
disabled={!isClickable}
onChange={async (e) => {
if (isUploadingLogo) return
const file = e.target.files?.[0]
if (!file) return
try {
await uploadTeamLogo(file)
} 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 = ''
}
}}
/>
)}
</div>
<div className="flex items-center gap-2">
{isEditingName ? (
<>
<input
type="text"
value={editedName}
onChange={(e) => 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"
/>
<Button
title="Übernehmen"
color="green"
size="sm"
variant="soft"
onClick={async () => {
await renameTeam(team.id, editedName)
setIsEditingName(false)
await handleReload()
}}
className="h-[34px] px-3 flex items-center justify-center"
>
</Button>
<Button
title="Abbrechen"
color="red"
size="sm"
variant="ghost"
onClick={() => {
setIsEditingName(false)
setEditedName(team.name ?? '')
}}
className="h-[34px] px-3 flex items-center justify-center"
>
</Button>
</>
) : (
<>
<div className="flex items-center gap-3">
<h2 className="text-lg font-semibold text-gray-800 dark:text-white">
{team.name ?? 'Team'}
</h2>
<TeamPremierRankBadge players={activePlayers} />
</div>
{canManage && (
<Button
title="Bearbeiten"
color="blue"
size="sm"
variant="soft"
onClick={() => {
setIsEditingName(true)
setEditedName(team.name || '')
}}
className="h-[34px] px-3 flex items-center justify-center"
>
Bearbeiten
</Button>
)}
</>
)}
</div>
</div>
<div className="flex gap-2">
{canManage && (
<Button onClick={() => setShowDeleteModal(true)} color='red' size='sm'>
Team löschen
</Button>
)}
<Button
onClick={async () => {
if (isLeader) setShowLeaveModal(true)
else {
try { await leaveTeam(currentUserSteamId) }
catch (err) { console.error('Fehler beim Verlassen:', err) }
}
}}
color='blue'
size='sm'
>
Team verlassen
</Button>
</div>
</div>
<DndContext
key={`dnd-${team.id}-${remountKey}`}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="space-y-8">
<DroppableZone id="active" label={`Aktive Spieler (${activePlayers.length} / 5)`} activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
<SortableContext id="active" key={`sc-active-${remountKey}-${activePlayers.map(p=>p.steamId).join(',')}`} items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
{renderMemberList(activePlayers)}
</SortableContext>
</DroppableZone>
<DroppableZone id="inactive" label="Inaktive Spieler" activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
<SortableContext id="inactive" key={`sc-inactive-${remountKey}-${inactivePlayers.map(p=>p.steamId).join(',')}`} items={inactivePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
{renderMemberList(inactivePlayers)}
{canManage && (
<motion.div key="mini-card-dummy" initial={{ opacity: 0 }} animate={{ opacity: 1}} exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
<MiniCardDummy
zoneId="inactive"
title={canAddDirect ? 'Hinzufügen' : 'Einladen'}
onClick={() => {
setShowInviteModal(false)
setTimeout(() => setShowInviteModal(true), 0)
}}
>
<div className="flex items-center justify-center w-16 h-16 bg-white rounded-full text-black">
<svg xmlns="http://www.w3.org/2000/svg" className="w-8 h-8" viewBox="0 0 640 512" fill="currentColor">
<path d="M96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM0 482.3C0 383.8 79.8 304 178.3 304h91.4C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3zM504 312v-64h-64c-13.3 0-24-10.7-24-24s10.7-24 24-24h64v-64c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24h-64v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z" />
</svg>
</div>
</MiniCardDummy>
</motion.div>
)}
</SortableContext>
</DroppableZone>
{invitedPlayers.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-md font-semibold text-gray-700 dark:text-gray-300">Eingeladene Spieler</h3>
</div>
<div className="w-full rounded-lg p-4 transition-colors min-h-[200px] border border-gray-300 dark:border-neutral-700">
<div className="grid gap-4 justify-start grid-cols-[repeat(auto-fill,minmax(160px,1fr))]">
<AnimatePresence>
{invitedPlayers.map((player: InvitedPlayer) => (
<motion.div key={player.steamId} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} transition={{ duration: 0.2 }}>
<MiniCard
steamId={player.steamId}
title={player.name}
avatar={player.avatar}
location={player.location}
selected={false}
onSelect={() => {}}
draggable={false}
currentUserSteamId={currentUserSteamId}
teamLeaderSteamId={team.leader}
isSelectable={false}
isInvite={true}
rank={player.premierRank}
invitationId={(player as any).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 any).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)
}
}}
/>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
</div>
)}
</div>
<DragOverlay>
{activeDragItem && (
<SortableMiniCard
player={activeDragItem}
currentUserSteamId={currentUserSteamId}
teamLeaderSteamId={team.leader}
isAdmin={adminMode}
hideOverlay
matchParentBg
/>
)}
</DragOverlay>
</DndContext>
{canInvite && (
<InvitePlayersModal
show={showInviteModal}
onClose={() => setShowInviteModal(false)}
onSuccess={() => {}}
team={team}
/>
)}
{canAddDirect && (
<InvitePlayersModal
show={showInviteModal}
onClose={() => setShowInviteModal(false)}
onSuccess={() => {}}
team={team}
directAdd
/>
)}
{isLeader && (
<LeaveTeamModal
show={showLeaveModal}
onClose={() => setShowLeaveModal(false)}
onSuccess={() => setShowLeaveModal(false)}
team={team}
/>
)}
{canManage && promoteCandidate && (
<Modal
id={`modal-promote-player-${promoteCandidate.steamId}`}
title="Leader übertragen"
show={true}
onClose={() => setPromoteCandidate(null)}
onSave={async () => {
await promoteToLeader(promoteCandidate.steamId)
setPromoteCandidate(null)
}}
closeButtonTitle="Übertragen"
closeButtonColor="blue"
>
<div className="flex justify-center mb-4">
<MiniCard
steamId={promoteCandidate.steamId}
title={promoteCandidate.name}
avatar={promoteCandidate.avatar}
location={promoteCandidate.location}
selected={false}
onSelect={() => {}}
draggable={false}
currentUserSteamId={currentUserSteamId}
teamLeaderSteamId={team.leader}
hideActions
isSelectable={false}
/>
</div>
<p className="text-sm text-gray-700 dark:text-neutral-300">
Möchtest du <strong>{promoteCandidate.name}</strong> wirklich zum Team-Leader machen?
</p>
</Modal>
)}
{canManage && kickCandidate && (
<Modal
id={`modal-kick-player-${kickCandidate.steamId}`}
title="Mitglied entfernen"
show={true}
onClose={() => setKickCandidate(null)}
onSave={confirmKick}
closeButtonTitle="Entfernen"
closeButtonColor="red"
>
<div className="flex justify-center mb-4">
<MiniCard
steamId={kickCandidate.steamId}
title={kickCandidate.name}
avatar={kickCandidate.avatar}
location={kickCandidate.location}
selected={false}
onSelect={() => {}}
draggable={false}
currentUserSteamId={currentUserSteamId}
teamLeaderSteamId={team.leader}
hideActions
isSelectable={false}
/>
</div>
<p className="text-sm text-gray-700 dark:text-neutral-300">
Möchtest du <strong>{kickCandidate.name}</strong> wirklich aus dem Team entfernen?
</p>
</Modal>
)}
{canManage && (
<Modal
id="modal-delete-team"
title="Team löschen"
show={showDeleteModal}
onClose={() => 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="Löschen"
closeButtonColor="red"
>
<p className="text-sm text-gray-700 dark:text-neutral-300">
Bist du sicher, dass du dieses Team löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.
</p>
</Modal>
)}
</div>
)
}