'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 = 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([]) 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) // Cache-Busting fürs Logo const [logoVersion, setLogoVersion] = useState(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(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((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[]) => ( {players.map(player => ( { if (isDragging) e.preventDefault() }} > setPromoteCandidate(player)} currentUserSteamId={manageSteam} teamLeaderSteamId={team.leader} 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)} > Teamlogo {/* Hover-Overlay nur, wenn klickbar */} {canManage && isClickable && (
)} {/* Progress-Kreis (Start bei 12 Uhr via rotate(-90 …)) */} {isUploadingLogo && (
{uploadPct}%
)}
{canManage && ( { 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 = '' } }} /> )}
{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 && ( )} )}
{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 && ( { setShowInviteModal(false) setTimeout(() => setShowInviteModal(true), 0) }} >
)}
{invitedPlayers.length > 0 && (

Eingeladene Spieler

{invitedPlayers.map((player: InvitedPlayer) => ( {}} 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) } }} /> ))}
)}
{activeDragItem && ( )}
{canInvite && ( setShowInviteModal(false)} onSuccess={() => {}} team={team} /> )} {canAddDirect && ( setShowInviteModal(false)} onSuccess={() => {}} 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} currentUserSteamId={currentUserSteamId} teamLeaderSteamId={team.leader} 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} currentUserSteamId={currentUserSteamId} teamLeaderSteamId={team.leader} 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="Löschen" closeButtonColor="red" >

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

)}
) }