diff --git a/public/assets/img/logos/fc1d7902-5c08-4e3a-8c42-36b6f9efc7f2-8319714c-536c-4aaf-abc3-546a2b0d15c4.png b/public/assets/img/logos/fc1d7902-5c08-4e3a-8c42-36b6f9efc7f2-8319714c-536c-4aaf-abc3-546a2b0d15c4.png deleted file mode 100644 index 4a23667..0000000 Binary files a/public/assets/img/logos/fc1d7902-5c08-4e3a-8c42-36b6f9efc7f2-8319714c-536c-4aaf-abc3-546a2b0d15c4.png and /dev/null differ diff --git a/public/assets/img/logos/fc1d7902-5c08-4e3a-8c42-36b6f9efc7f2-e0e567f6-678d-4999-9580-0c88c6da6b3e.jpg b/public/assets/img/logos/fc1d7902-5c08-4e3a-8c42-36b6f9efc7f2-e0e567f6-678d-4999-9580-0c88c6da6b3e.jpg new file mode 100644 index 0000000..b7c9974 Binary files /dev/null and b/public/assets/img/logos/fc1d7902-5c08-4e3a-8c42-36b6f9efc7f2-e0e567f6-678d-4999-9580-0c88c6da6b3e.jpg differ diff --git a/src/app/components/TeamMemberView.tsx b/src/app/components/TeamMemberView.tsx index 54c5845..05fdad9 100644 --- a/src/app/components/TeamMemberView.tsx +++ b/src/app/components/TeamMemberView.tsx @@ -12,7 +12,7 @@ import InvitePlayersModal from './InvitePlayersModal' import Modal from './Modal' import { Player } from '../types/team' 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 Image from 'next/image' import TeamPremierRankBadge from './TeamPremierRankBadge' @@ -61,15 +61,12 @@ export default function TeamMemberView({ const team = teamProp ?? storeTeam if (!team) return null - const RELEVANT: ReadonlySet = new Set([ - ...TEAM_EVENTS, - ...SELF_EVENTS, - ]) + 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 isLeader = currentUserSteamId === team.leader + const canManage = adminMode || isLeader + const canInvite = isLeader && !adminMode + const canAddDirect = adminMode const isDraggingRef = useRef(false) const [pendingRemote, setPendingRemote] = useState<{ @@ -78,7 +75,7 @@ export default function TeamMemberView({ invited: InvitedPlayer[] } | null>(null) const [remountKey, setRemountKey] = useState(0) - const { connect, disconnect, lastEvent, isConnected } = useSSEStore() + const { connect, lastEvent, isConnected } = useSSEStore() const [activePlayers, setActivePlayers] = useState([]) const [inactivePlayers, setInactivePlayers] = useState([]) const [invitedPlayers, setInvitedPlayers] = useState([]) @@ -89,26 +86,35 @@ export default function TeamMemberView({ const [editedName, setEditedName] = useState(team.name || '') 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 useEffect(() => { if (!currentUserSteamId) return if (!isConnected) connect(currentUserSteamId) - return () => { - // Falls du global verbunden bleiben willst: disconnect() hier weglassen - // disconnect() - } }, [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(() => { 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)) - // wenn nichts geändert: nichts tun (vermeidet Flicker) const unchanged = eqByIds(activePlayers, nextActive) && eqByIds(inactivePlayers, nextInactive) && @@ -117,30 +123,23 @@ export default function TeamMemberView({ if (unchanged) return if (!isDraggingRef.current) { - // 🔄 nicht am Ziehen → direkt übernehmen setActivePlayers(nextActive) setInactivePlayers(nextInactive) setInvitedPlayers(nextInvited) } else { - // ✋ während Drag → puffern 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]) + }, [team?.id, team?.name, team?.logo, team?.leader, team?.activePlayers, team?.inactivePlayers, team?.invitedPlayers]) - // 2) Auf ALLE relevanten Events reagieren + // 2) Relevante SSE-Events -> Team neu laden useEffect(() => { if (!lastEvent || !team?.id) return - - // Typ-Safety: nur kanonische Events weiter verarbeiten if (!isSseEventType(lastEvent.type)) return - const evtType: SSEEventType = lastEvent.type - - if (!RELEVANT.has(evtType)) return + if (!RELEVANT.has(lastEvent.type)) return const payload = lastEvent.payload ?? {} - if (payload.teamId !== team.id) return + if (payload.teamId && payload.teamId !== team.id) return ;(async () => { const updated = await reloadTeam(team.id) @@ -155,12 +154,9 @@ export default function TeamMemberView({ ).sort((a,b)=>a.name.localeCompare(b.name)) if (isDraggingRef.current) { - // während Drag: puffern, kein Remount setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited }) return } - - // sofort in die lokalen DnD-Listen übernehmen + DnD intern neu aufbauen setActivePlayers(nextActive) setInactivePlayers(nextInactive) setInvitedPlayers(nextInvited) @@ -168,16 +164,8 @@ export default function TeamMemberView({ })() }, [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 id = event.active.id + const id = event.active.id as string const item = activePlayers.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') - // 👇 Fail-safe: eigenes Team sofort frisch holen & in den Store schieben const updated = await reloadTeam(teamId) if (updated) setTeam(updated) } catch (err) { @@ -209,7 +196,6 @@ export default function TeamMemberView({ } } - const handleDragEnd = async (event: any) => { setActiveDragItem(null) setIsDragging(false) @@ -217,7 +203,6 @@ export default function TeamMemberView({ const { active, over } = event if (!over) { - // falls während des Drags Remote-Updates ankamen → jetzt anwenden if (pendingRemote) { setActivePlayers(pendingRemote.active) setInactivePlayers(pendingRemote.inactive) @@ -235,14 +220,11 @@ export default function TeamMemberView({ inactivePlayers.find(p => p.steamId === activeId) if (!movingItem) return - // Woher kam die Karte? const wasInActive = activePlayers.some(p => p.steamId === activeId) - - // Wohin wurde gedroppt? const dropToActive = 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 (pendingRemote) { setActivePlayers(pendingRemote.active) @@ -253,7 +235,6 @@ export default function TeamMemberView({ return } - // ── tatsächliche Zonen-Änderung ────────────────────────────────── let nextActive = [...activePlayers] let nextInactive = [...inactivePlayers] @@ -266,11 +247,9 @@ export default function TeamMemberView({ 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)) nextInactive.sort((a,b)=>a.name.localeCompare(b.name)) - // 🔍 Sicherheit: Wenn sich durch die Operation nichts geändert hat → abbrechen const noChange = eqByIds(nextActive, activePlayers) && eqByIds(nextInactive, inactivePlayers) if (noChange) { @@ -283,17 +262,14 @@ export default function TeamMemberView({ return } - // ✅ Optimistisches UI setActivePlayers(nextActive) setInactivePlayers(nextInactive) - // 🔔 Server informieren updateTeamMembers(team.id, nextActive, nextInactive).catch(console.error) setSaveSuccess(true) setTimeout(()=>setSaveSuccess(false), 3000) - // 📨 evtl. gepufferte Remote-Änderungen übernehmen if (pendingRemote) { const diff = !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 updated = await reloadTeam(team.id) 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((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 const manageSteam: string = adminMode ? (team.leader ?? '') : currentUserSteamId @@ -392,7 +391,7 @@ export default function TeamMemberView({
canManage && document.getElementById('logoUpload')?.click()} + onClick={() => canManage && !isUploadingLogo && document.getElementById('logoUpload')?.click()} > - {canManage && ( -
- {/* Icon */} + + {canManage && !isUploadingLogo && ( +
)} + + {isUploadingLogo && ( +
+ + + + + {uploadPct}% +
+ )}
{canManage && ( @@ -422,13 +445,21 @@ export default function TeamMemberView({ onChange={async (e) => { const file = e.target.files?.[0] if (!file) return - const formData = new FormData() - formData.append('logo', file) - formData.append('teamId', team.id) - const res = await fetch('/api/team/upload-logo', { method: 'POST', body: formData }) - if (res.ok) await handleReload() - else alert('Fehler beim Hochladen des Logos.') + try { + await uploadTeamLogo(file) + await handleReload() + } 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 = '' + } }} + disabled={isUploadingLogo} /> )}
@@ -524,7 +555,12 @@ export default function TeamMemberView({
- +
p.steamId).join(',')}`} items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}> @@ -580,10 +616,10 @@ export default function TeamMemberView({ rank={player.premierRank} invitationId={(player as any).invitationId} onKick={async (sid) => { - // 1) optimistisch entfernen + // optimistisch entfernen setInvitedPlayers(list => list.filter(p => p.steamId !== sid)) try { - // 2) API: mit invitationId ODER Fallback teamId+steamId + // API: mit invitationId ODER Fallback teamId+steamId await fetch('/api/user/invitations/revoke', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -595,10 +631,10 @@ export default function TeamMemberView({ }) } catch (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))) } finally { - // 3) sicherheitshalber Team neu laden + // sicherheitshalber Team neu laden const updated = await reloadTeam(team.id) if (updated) setTeam(updated) } diff --git a/src/generated/prisma/edge.js b/src/generated/prisma/edge.js index fcf6176..0146bd1 100644 --- a/src/generated/prisma/edge.js +++ b/src/generated/prisma/edge.js @@ -303,7 +303,7 @@ const config = { "value": "prisma-client-js" }, "output": { - "value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma", + "value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma", "fromEnvVar": null }, "config": { @@ -317,7 +317,7 @@ const config = { } ], "previewFeatures": [], - "sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma", + "sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma", "isCustomOutput": true }, "relativeEnvPaths": { diff --git a/src/generated/prisma/index.js b/src/generated/prisma/index.js index 7ec88a4..96482e2 100644 --- a/src/generated/prisma/index.js +++ b/src/generated/prisma/index.js @@ -304,7 +304,7 @@ const config = { "value": "prisma-client-js" }, "output": { - "value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma", + "value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma", "fromEnvVar": null }, "config": { @@ -318,7 +318,7 @@ const config = { } ], "previewFeatures": [], - "sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma", + "sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma", "isCustomOutput": true }, "relativeEnvPaths": {