update
This commit is contained in:
parent
af9fe48584
commit
134955c829
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@ -12,7 +12,7 @@ import InvitePlayersModal from './InvitePlayersModal'
|
|||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import { Player } from '../types/team'
|
import { Player } from '../types/team'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { leaveTeam, reloadTeam, renameTeam, revokeInvitation } from '@/app/lib/sse-actions'
|
import { leaveTeam, reloadTeam, renameTeam } from '@/app/lib/sse-actions'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
||||||
@ -61,15 +61,12 @@ export default function TeamMemberView({
|
|||||||
const team = teamProp ?? storeTeam
|
const team = teamProp ?? storeTeam
|
||||||
if (!team) return null
|
if (!team) return null
|
||||||
|
|
||||||
const RELEVANT: ReadonlySet<SSEEventType> = new Set([
|
const RELEVANT: ReadonlySet<SSEEventType> = new Set([...TEAM_EVENTS, ...SELF_EVENTS])
|
||||||
...TEAM_EVENTS,
|
|
||||||
...SELF_EVENTS,
|
|
||||||
])
|
|
||||||
|
|
||||||
const isLeader = currentUserSteamId === team.leader
|
const isLeader = currentUserSteamId === team.leader
|
||||||
const canManage = adminMode || isLeader
|
const canManage = adminMode || isLeader
|
||||||
const canInvite = isLeader && !adminMode
|
const canInvite = isLeader && !adminMode
|
||||||
const canAddDirect= adminMode
|
const canAddDirect = adminMode
|
||||||
|
|
||||||
const isDraggingRef = useRef(false)
|
const isDraggingRef = useRef(false)
|
||||||
const [pendingRemote, setPendingRemote] = useState<{
|
const [pendingRemote, setPendingRemote] = useState<{
|
||||||
@ -78,7 +75,7 @@ export default function TeamMemberView({
|
|||||||
invited: InvitedPlayer[]
|
invited: InvitedPlayer[]
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [remountKey, setRemountKey] = useState(0)
|
const [remountKey, setRemountKey] = useState(0)
|
||||||
const { connect, disconnect, lastEvent, isConnected } = useSSEStore()
|
const { connect, lastEvent, isConnected } = useSSEStore()
|
||||||
const [activePlayers, setActivePlayers] = useState<Player[]>([])
|
const [activePlayers, setActivePlayers] = useState<Player[]>([])
|
||||||
const [inactivePlayers, setInactivePlayers] = useState<Player[]>([])
|
const [inactivePlayers, setInactivePlayers] = useState<Player[]>([])
|
||||||
const [invitedPlayers, setInvitedPlayers] = useState<InvitedPlayer[]>([])
|
const [invitedPlayers, setInvitedPlayers] = useState<InvitedPlayer[]>([])
|
||||||
@ -89,26 +86,35 @@ export default function TeamMemberView({
|
|||||||
const [editedName, setEditedName] = useState(team.name || '')
|
const [editedName, setEditedName] = useState(team.name || '')
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false)
|
const [saveSuccess, setSaveSuccess] = useState(false)
|
||||||
|
|
||||||
|
// Upload-Progress für Teamlogo
|
||||||
|
const [isUploadingLogo, setIsUploadingLogo] = useState(false)
|
||||||
|
const [uploadPct, setUploadPct] = useState(0)
|
||||||
|
const R = 28
|
||||||
|
const S = 64
|
||||||
|
const CIRC = 2 * Math.PI * R
|
||||||
|
const dashOffset = CIRC - (uploadPct / 100) * CIRC
|
||||||
|
|
||||||
// 1) SSE-Verbindung sicherstellen
|
// 1) SSE-Verbindung sicherstellen
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUserSteamId) return
|
if (!currentUserSteamId) return
|
||||||
if (!isConnected) connect(currentUserSteamId)
|
if (!isConnected) connect(currentUserSteamId)
|
||||||
return () => {
|
|
||||||
// Falls du global verbunden bleiben willst: disconnect() hier weglassen
|
|
||||||
// disconnect()
|
|
||||||
}
|
|
||||||
}, [currentUserSteamId, connect, isConnected])
|
}, [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
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!team) return
|
if (!team) return
|
||||||
|
|
||||||
const nextActive = (team.activePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
|
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 nextInactive = (team.inactivePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
|
||||||
const nextInvited = Array.from(
|
const nextInvited = Array.from(
|
||||||
new Map((team.invitedPlayers ?? []).map(p => [p.steamId, p])).values()
|
new Map((team.invitedPlayers ?? []).map(p => [p.steamId, p])).values()
|
||||||
).sort((a,b)=>a.name.localeCompare(b.name))
|
).sort((a,b)=>a.name.localeCompare(b.name))
|
||||||
|
|
||||||
// wenn nichts geändert: nichts tun (vermeidet Flicker)
|
|
||||||
const unchanged =
|
const unchanged =
|
||||||
eqByIds(activePlayers, nextActive) &&
|
eqByIds(activePlayers, nextActive) &&
|
||||||
eqByIds(inactivePlayers, nextInactive) &&
|
eqByIds(inactivePlayers, nextInactive) &&
|
||||||
@ -117,30 +123,23 @@ export default function TeamMemberView({
|
|||||||
if (unchanged) return
|
if (unchanged) return
|
||||||
|
|
||||||
if (!isDraggingRef.current) {
|
if (!isDraggingRef.current) {
|
||||||
// 🔄 nicht am Ziehen → direkt übernehmen
|
|
||||||
setActivePlayers(nextActive)
|
setActivePlayers(nextActive)
|
||||||
setInactivePlayers(nextInactive)
|
setInactivePlayers(nextInactive)
|
||||||
setInvitedPlayers(nextInvited)
|
setInvitedPlayers(nextInvited)
|
||||||
} else {
|
} else {
|
||||||
// ✋ während Drag → puffern
|
|
||||||
setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited })
|
setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited })
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [team?.id, team?.name, team?.logo, team?.leader,
|
}, [team?.id, team?.name, team?.logo, team?.leader, team?.activePlayers, team?.inactivePlayers, team?.invitedPlayers])
|
||||||
team?.activePlayers, team?.inactivePlayers, team?.invitedPlayers])
|
|
||||||
|
|
||||||
// 2) Auf ALLE relevanten Events reagieren
|
// 2) Relevante SSE-Events -> Team neu laden
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastEvent || !team?.id) return
|
if (!lastEvent || !team?.id) return
|
||||||
|
|
||||||
// Typ-Safety: nur kanonische Events weiter verarbeiten
|
|
||||||
if (!isSseEventType(lastEvent.type)) return
|
if (!isSseEventType(lastEvent.type)) return
|
||||||
const evtType: SSEEventType = lastEvent.type
|
if (!RELEVANT.has(lastEvent.type)) return
|
||||||
|
|
||||||
if (!RELEVANT.has(evtType)) return
|
|
||||||
|
|
||||||
const payload = lastEvent.payload ?? {}
|
const payload = lastEvent.payload ?? {}
|
||||||
if (payload.teamId !== team.id) return
|
if (payload.teamId && payload.teamId !== team.id) return
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const updated = await reloadTeam(team.id)
|
const updated = await reloadTeam(team.id)
|
||||||
@ -155,12 +154,9 @@ export default function TeamMemberView({
|
|||||||
).sort((a,b)=>a.name.localeCompare(b.name))
|
).sort((a,b)=>a.name.localeCompare(b.name))
|
||||||
|
|
||||||
if (isDraggingRef.current) {
|
if (isDraggingRef.current) {
|
||||||
// während Drag: puffern, kein Remount
|
|
||||||
setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited })
|
setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// sofort in die lokalen DnD-Listen übernehmen + DnD intern neu aufbauen
|
|
||||||
setActivePlayers(nextActive)
|
setActivePlayers(nextActive)
|
||||||
setInactivePlayers(nextInactive)
|
setInactivePlayers(nextInactive)
|
||||||
setInvitedPlayers(nextInvited)
|
setInvitedPlayers(nextInvited)
|
||||||
@ -168,16 +164,8 @@ export default function TeamMemberView({
|
|||||||
})()
|
})()
|
||||||
}, [lastEvent, team?.id, setTeam])
|
}, [lastEvent, team?.id, setTeam])
|
||||||
|
|
||||||
|
|
||||||
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 handleDragStart = (event: any) => {
|
const handleDragStart = (event: any) => {
|
||||||
const id = event.active.id
|
const id = event.active.id as string
|
||||||
const item =
|
const item =
|
||||||
activePlayers.find(p => p.steamId === id) ||
|
activePlayers.find(p => p.steamId === id) ||
|
||||||
inactivePlayers.find(p => p.steamId === id)
|
inactivePlayers.find(p => p.steamId === id)
|
||||||
@ -201,7 +189,6 @@ export default function TeamMemberView({
|
|||||||
})
|
})
|
||||||
if (!res.ok) throw new Error('Update fehlgeschlagen')
|
if (!res.ok) throw new Error('Update fehlgeschlagen')
|
||||||
|
|
||||||
// 👇 Fail-safe: eigenes Team sofort frisch holen & in den Store schieben
|
|
||||||
const updated = await reloadTeam(teamId)
|
const updated = await reloadTeam(teamId)
|
||||||
if (updated) setTeam(updated)
|
if (updated) setTeam(updated)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -209,7 +196,6 @@ export default function TeamMemberView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const handleDragEnd = async (event: any) => {
|
const handleDragEnd = async (event: any) => {
|
||||||
setActiveDragItem(null)
|
setActiveDragItem(null)
|
||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
@ -217,7 +203,6 @@ export default function TeamMemberView({
|
|||||||
|
|
||||||
const { active, over } = event
|
const { active, over } = event
|
||||||
if (!over) {
|
if (!over) {
|
||||||
// falls während des Drags Remote-Updates ankamen → jetzt anwenden
|
|
||||||
if (pendingRemote) {
|
if (pendingRemote) {
|
||||||
setActivePlayers(pendingRemote.active)
|
setActivePlayers(pendingRemote.active)
|
||||||
setInactivePlayers(pendingRemote.inactive)
|
setInactivePlayers(pendingRemote.inactive)
|
||||||
@ -235,14 +220,11 @@ export default function TeamMemberView({
|
|||||||
inactivePlayers.find(p => p.steamId === activeId)
|
inactivePlayers.find(p => p.steamId === activeId)
|
||||||
if (!movingItem) return
|
if (!movingItem) return
|
||||||
|
|
||||||
// Woher kam die Karte?
|
|
||||||
const wasInActive = activePlayers.some(p => p.steamId === activeId)
|
const wasInActive = activePlayers.some(p => p.steamId === activeId)
|
||||||
|
|
||||||
// Wohin wurde gedroppt?
|
|
||||||
const dropToActive =
|
const dropToActive =
|
||||||
overId === 'active' || activePlayers.some(p => p.steamId === overId)
|
overId === 'active' || activePlayers.some(p => p.steamId === overId)
|
||||||
|
|
||||||
// 🚫 Selbe Zone -> nichts speichern, nur evtl. pendingRemote anwenden
|
// selbe Zone -> nichts speichern
|
||||||
if ((wasInActive && dropToActive) || (!wasInActive && !dropToActive)) {
|
if ((wasInActive && dropToActive) || (!wasInActive && !dropToActive)) {
|
||||||
if (pendingRemote) {
|
if (pendingRemote) {
|
||||||
setActivePlayers(pendingRemote.active)
|
setActivePlayers(pendingRemote.active)
|
||||||
@ -253,7 +235,6 @@ export default function TeamMemberView({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── tatsächliche Zonen-Änderung ──────────────────────────────────
|
|
||||||
let nextActive = [...activePlayers]
|
let nextActive = [...activePlayers]
|
||||||
let nextInactive = [...inactivePlayers]
|
let nextInactive = [...inactivePlayers]
|
||||||
|
|
||||||
@ -266,11 +247,9 @@ export default function TeamMemberView({
|
|||||||
if (!nextInactive.some(p => p.steamId === activeId)) nextInactive.push(movingItem)
|
if (!nextInactive.some(p => p.steamId === activeId)) nextInactive.push(movingItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deine UI sortiert eh alphabetisch → Reihenfolge innerhalb der Zone ist egal
|
|
||||||
nextActive.sort((a,b)=>a.name.localeCompare(b.name))
|
nextActive.sort((a,b)=>a.name.localeCompare(b.name))
|
||||||
nextInactive.sort((a,b)=>a.name.localeCompare(b.name))
|
nextInactive.sort((a,b)=>a.name.localeCompare(b.name))
|
||||||
|
|
||||||
// 🔍 Sicherheit: Wenn sich durch die Operation nichts geändert hat → abbrechen
|
|
||||||
const noChange =
|
const noChange =
|
||||||
eqByIds(nextActive, activePlayers) && eqByIds(nextInactive, inactivePlayers)
|
eqByIds(nextActive, activePlayers) && eqByIds(nextInactive, inactivePlayers)
|
||||||
if (noChange) {
|
if (noChange) {
|
||||||
@ -283,17 +262,14 @@ export default function TeamMemberView({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Optimistisches UI
|
|
||||||
setActivePlayers(nextActive)
|
setActivePlayers(nextActive)
|
||||||
setInactivePlayers(nextInactive)
|
setInactivePlayers(nextInactive)
|
||||||
|
|
||||||
// 🔔 Server informieren
|
|
||||||
updateTeamMembers(team.id, nextActive, nextInactive).catch(console.error)
|
updateTeamMembers(team.id, nextActive, nextInactive).catch(console.error)
|
||||||
|
|
||||||
setSaveSuccess(true)
|
setSaveSuccess(true)
|
||||||
setTimeout(()=>setSaveSuccess(false), 3000)
|
setTimeout(()=>setSaveSuccess(false), 3000)
|
||||||
|
|
||||||
// 📨 evtl. gepufferte Remote-Änderungen übernehmen
|
|
||||||
if (pendingRemote) {
|
if (pendingRemote) {
|
||||||
const diff =
|
const diff =
|
||||||
!eqByIds(pendingRemote.active, nextActive) ||
|
!eqByIds(pendingRemote.active, nextActive) ||
|
||||||
@ -308,7 +284,6 @@ export default function TeamMemberView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn du von hier manuell reloaden willst, dann Store updaten (nicht lokalen State)
|
|
||||||
const handleReload = async () => {
|
const handleReload = async () => {
|
||||||
const updated = await reloadTeam(team.id)
|
const updated = await reloadTeam(team.id)
|
||||||
if (updated) setTeam(updated)
|
if (updated) setTeam(updated)
|
||||||
@ -348,6 +323,30 @@ export default function TeamMemberView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload mit Progress via XHR
|
||||||
|
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') // ggf. anpassen
|
||||||
|
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 = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : 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
|
if (!adminMode && !currentUserSteamId) return null
|
||||||
|
|
||||||
const manageSteam: string = adminMode ? (team.leader ?? '') : currentUserSteamId
|
const manageSteam: string = adminMode ? (team.leader ?? '') : currentUserSteamId
|
||||||
@ -392,7 +391,7 @@ export default function TeamMemberView({
|
|||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div
|
<div
|
||||||
className="relative w-16 h-16 rounded-full overflow-hidden border border-gray-300 dark:border-neutral-600 cursor-pointer"
|
className="relative w-16 h-16 rounded-full overflow-hidden border border-gray-300 dark:border-neutral-600 cursor-pointer"
|
||||||
onClick={() => canManage && document.getElementById('logoUpload')?.click()}
|
onClick={() => canManage && !isUploadingLogo && document.getElementById('logoUpload')?.click()}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={team.logo ? `/assets/img/logos/${team.logo}` : `/assets/img/logos/cs2.webp`}
|
src={team.logo ? `/assets/img/logos/${team.logo}` : `/assets/img/logos/cs2.webp`}
|
||||||
@ -400,17 +399,41 @@ export default function TeamMemberView({
|
|||||||
fill
|
fill
|
||||||
sizes="64px"
|
sizes="64px"
|
||||||
quality={75}
|
quality={75}
|
||||||
className="object-cover"
|
className={`object-cover ${isUploadingLogo ? 'opacity-70' : ''}`}
|
||||||
priority={false}
|
priority={false}
|
||||||
/>
|
/>
|
||||||
{canManage && (
|
|
||||||
<div className="absolute inset-0 bg-black bg-opacity-50 text-white flex flex-col items-center justify-center text-xs font-medium opacity-0 group-hover:opacity-100 transition-opacity">
|
{canManage && !isUploadingLogo && (
|
||||||
{/* Icon */}
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{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">
|
||||||
|
<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' }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-[11px] font-semibold text-white drop-shadow">{uploadPct}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canManage && (
|
{canManage && (
|
||||||
@ -422,13 +445,21 @@ export default function TeamMemberView({
|
|||||||
onChange={async (e) => {
|
onChange={async (e) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
const formData = new FormData()
|
try {
|
||||||
formData.append('logo', file)
|
await uploadTeamLogo(file)
|
||||||
formData.append('teamId', team.id)
|
await handleReload()
|
||||||
const res = await fetch('/api/team/upload-logo', { method: 'POST', body: formData })
|
} catch (err) {
|
||||||
if (res.ok) await handleReload()
|
console.error('Fehler beim Hochladen des Logos:', err)
|
||||||
else alert('Fehler beim Hochladen des Logos.')
|
alert('Fehler beim Hochladen des Logos.')
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUploadingLogo(false)
|
||||||
|
setUploadPct(0)
|
||||||
|
}, 300)
|
||||||
|
e.currentTarget.value = ''
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={isUploadingLogo}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -524,7 +555,12 @@ export default function TeamMemberView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DndContext key={`dnd-${team.id}-${remountKey}`} collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
<DndContext
|
||||||
|
key={`dnd-${team.id}-${remountKey}`}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<DroppableZone id="active" label={`Aktive Spieler (${activePlayers.length} / 5)`} activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
<DroppableZone id="active" label={`Aktive Spieler (${activePlayers.length} / 5)`} activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
||||||
<SortableContext key={`sc-active-${remountKey}-${activePlayers.map(p=>p.steamId).join(',')}`} items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
<SortableContext key={`sc-active-${remountKey}-${activePlayers.map(p=>p.steamId).join(',')}`} items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||||
@ -580,10 +616,10 @@ export default function TeamMemberView({
|
|||||||
rank={player.premierRank}
|
rank={player.premierRank}
|
||||||
invitationId={(player as any).invitationId}
|
invitationId={(player as any).invitationId}
|
||||||
onKick={async (sid) => {
|
onKick={async (sid) => {
|
||||||
// 1) optimistisch entfernen
|
// optimistisch entfernen
|
||||||
setInvitedPlayers(list => list.filter(p => p.steamId !== sid))
|
setInvitedPlayers(list => list.filter(p => p.steamId !== sid))
|
||||||
try {
|
try {
|
||||||
// 2) API: mit invitationId ODER Fallback teamId+steamId
|
// API: mit invitationId ODER Fallback teamId+steamId
|
||||||
await fetch('/api/user/invitations/revoke', {
|
await fetch('/api/user/invitations/revoke', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@ -595,10 +631,10 @@ export default function TeamMemberView({
|
|||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Revoke fehlgeschlagen:', e)
|
console.error('Revoke fehlgeschlagen:', e)
|
||||||
// optional: bei Fehler wieder einfügen
|
// bei Fehler wieder einfügen
|
||||||
setInvitedPlayers(list => [...list, player].sort((a,b)=>a.name.localeCompare(b.name)))
|
setInvitedPlayers(list => [...list, player].sort((a,b)=>a.name.localeCompare(b.name)))
|
||||||
} finally {
|
} finally {
|
||||||
// 3) sicherheitshalber Team neu laden
|
// sicherheitshalber Team neu laden
|
||||||
const updated = await reloadTeam(team.id)
|
const updated = await reloadTeam(team.id)
|
||||||
if (updated) setTeam(updated)
|
if (updated) setTeam(updated)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -303,7 +303,7 @@ const config = {
|
|||||||
"value": "prisma-client-js"
|
"value": "prisma-client-js"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||||
"fromEnvVar": null
|
"fromEnvVar": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@ -317,7 +317,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"previewFeatures": [],
|
"previewFeatures": [],
|
||||||
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
|
|||||||
@ -304,7 +304,7 @@ const config = {
|
|||||||
"value": "prisma-client-js"
|
"value": "prisma-client-js"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||||
"fromEnvVar": null
|
"fromEnvVar": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@ -318,7 +318,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"previewFeatures": [],
|
"previewFeatures": [],
|
||||||
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user