diff --git a/src/app/admin/teams/[teamId]/TeamAdminClient.tsx b/src/app/admin/teams/[teamId]/TeamAdminClient.tsx index ea3fc8f..3c95aa4 100644 --- a/src/app/admin/teams/[teamId]/TeamAdminClient.tsx +++ b/src/app/admin/teams/[teamId]/TeamAdminClient.tsx @@ -1,30 +1,48 @@ -// ─────────────────────────────────────────────────────────── -// src/app/(admin)/admin/teams/[teamId]/TeamAdminClient.tsx -// ─────────────────────────────────────────────────────────── 'use client' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useSession } from 'next-auth/react' - -import LoadingSpinner from '@/app/components/LoadingSpinner' -import TeamMemberView from '@/app/components/TeamMemberView' -//import { useTeamManager } from '@/app/hooks/useTeamManager' +import LoadingSpinner from '@/app/components/LoadingSpinner' +import TeamMemberView from '@/app/components/TeamMemberView' +import { useTeamStore } from '@/app/lib/stores' +import { reloadTeam } from '@/app/lib/sse-actions' export default function TeamAdminClient({ teamId }: { teamId: string }) { - const [refetchKey, setRefetchKey] = useState() + const [loading, setLoading] = useState(true) const { data: session } = useSession() + const { team, setTeam } = useTeamStore() - // jetzt wird die ID korrekt übergeben ➜ /api/team/[id] - //const teamManager = useTeamManager({ teamId, refetchKey }, null) + useEffect(() => { + const fetch = async () => { + const result = await reloadTeam(teamId) + console.log('[TeamAdminClient] reloadTeam returned:', result) + if (result) { + setTeam(result) + } + setLoading(false) + } - if (teamManager.isLoading) return + if (teamId) fetch() + }, [teamId, setTeam]) + + if (loading || !team) { + return + } return (
{}} + setShowInviteModal={() => {}} + setActiveDragItem={() => {}} + setIsDragging={() => {}} />
) diff --git a/src/app/components/LeaveTeamModal.tsx b/src/app/components/LeaveTeamModal.tsx index db0bc9e..ccf5c52 100644 --- a/src/app/components/LeaveTeamModal.tsx +++ b/src/app/components/LeaveTeamModal.tsx @@ -5,7 +5,7 @@ import Modal from './Modal' import MiniCard from './MiniCard' import { useSession } from 'next-auth/react' import { Player, Team } from '../types/team' -import { leaveTeam } from '../lib/team-actions' +import { leaveTeam } from '../lib/sse-actions' type Props = { show: boolean diff --git a/src/app/components/MiniCard.tsx b/src/app/components/MiniCard.tsx index ab949c6..a1f2644 100644 --- a/src/app/components/MiniCard.tsx +++ b/src/app/components/MiniCard.tsx @@ -4,7 +4,7 @@ import Button from './Button' import Image from 'next/image' import PremierRankBadge from './PremierRankBadge' -import { revokeInvitation } from '../lib/team-actions' +import { revokeInvitation } from '../lib/sse-actions' import { motion, AnimatePresence } from 'framer-motion' type MiniCardProps = { diff --git a/src/app/components/NotificationCenter.tsx b/src/app/components/NotificationCenter.tsx index 9d1260d..fd48451 100644 --- a/src/app/components/NotificationCenter.tsx +++ b/src/app/components/NotificationCenter.tsx @@ -2,9 +2,7 @@ import { useEffect, useState } from 'react' import NotificationDropdown from './NotificationDropdown' -import { useSSE } from '@/app/lib/useSSEStore' import { useSession } from 'next-auth/react' -import { useTeamManager } from '../hooks/useTeamManager' import { useRouter } from 'next/navigation' /* ────────────────────────────────────────────────────────── */ @@ -27,7 +25,6 @@ export default function NotificationCenter() { const { data: session } = useSession() const [notifications, setNotifications] = useState([]) const [open, setOpen] = useState(false) - const { source, connect } = useSSE() //const { markAllAsRead, markOneAsRead, handleInviteAction } = useTeamManager({}, null) const router = useRouter() const [previewText, setPreviewText] = useState(null) @@ -70,12 +67,12 @@ export default function NotificationCenter() { } loadNotifications() - connect(steamId) // SSE starten - }, [session?.user?.steamId, connect]) + }, [session?.user?.steamId]) /* --- Live-Updates über SSE empfangen -------------------- */ useEffect(() => { - if (!source) return + return; + //if (!source) return /* Handler für JEDES eintreffende Paket ------------------ */ const handleEvent = (event: MessageEvent) => { @@ -129,17 +126,17 @@ export default function NotificationCenter() { ] /* Named Events abonnieren ------------------------------ */ - eventNames.forEach(evt => source.addEventListener(evt, handleEvent)) + //eventNames.forEach(evt => source.addEventListener(evt, handleEvent)) /* Fallback: Server sendet evtl. Events ohne „event:“----- */ - source.onmessage = handleEvent + //source.onmessage = handleEvent /* Aufräumen bei Unmount -------------------------------- */ return () => { - eventNames.forEach(evt => source.removeEventListener(evt, handleEvent)) - source.onmessage = null + //eventNames.forEach(evt => source.removeEventListener(evt, handleEvent)) + //source.onmessage = null } - }, [source]) + }, /*[source] */) /* ────────────────────────────────────────────────────────── */ /* Render */ diff --git a/src/app/components/SSEListener.tsx b/src/app/components/SSEListener.tsx deleted file mode 100644 index 79f0649..0000000 --- a/src/app/components/SSEListener.tsx +++ /dev/null @@ -1,81 +0,0 @@ -'use client' - -import { useSession } from 'next-auth/react' -import { useEffect } from 'react' -import { useSSE } from '@/app/lib/useSSEStore' - -export default function SSEListener() { - const { data: session } = useSession() - const connect = useSSE((s) => s.connect) - const disconnect = useSSE((s) => s.disconnect) - - useEffect(() => { - const steamId = session?.user?.steamId - if (!steamId) return - - const eventSource = connect(steamId) - if (!eventSource) return - - eventSource.onmessage = (event) => { - try { - console.error('[SSE] Nachricht empfangen:', event.data) - const data = JSON.parse(event.data) - - switch (data.type) { - case 'invitation': - window.dispatchEvent(new CustomEvent('ws-invitation')) - break - case 'team-update': - window.dispatchEvent(new CustomEvent('ws-team-update')) - break - case 'team-kick': - window.dispatchEvent(new CustomEvent('ws-team-kick')) - break - case 'team-kick-other': - window.dispatchEvent(new CustomEvent('ws-team-kick-other')) - break - case 'team-joined': - window.dispatchEvent(new CustomEvent('ws-team-joined')) - break - case 'team-member-joined': - window.dispatchEvent(new CustomEvent('ws-team-member-joined')) - break - case 'team-invite': - window.dispatchEvent(new CustomEvent('ws-team-invite')) - break - case 'team-invite-reject': - window.dispatchEvent(new CustomEvent('ws-team-invite-reject')) - break - case 'team-left': - window.dispatchEvent(new CustomEvent('ws-team-left')) - break - case 'team-member-left': - window.dispatchEvent(new CustomEvent('ws-team-member-left')) - break - case 'team-leader-changed': - window.dispatchEvent(new CustomEvent('ws-team-leader-changed')) - break - case 'team-join-request': - window.dispatchEvent(new CustomEvent('ws-team-join-request')) - break - case 'team-renamed': - window.dispatchEvent(new CustomEvent('ws-team-renamed', { - detail: { teamId: data.teamId } - })) - break - case 'team-logo-updated': - window.dispatchEvent(new CustomEvent('ws-team-logo-updated', { - detail: { teamId: data.teamId } - })) - break - } - } catch (err) { - console.error('[SSE] Ungültige Nachricht:', event.data) - } - } - - return () => disconnect() - }, [session?.user?.steamId]) - - return null -} diff --git a/src/app/components/TeamCard.tsx b/src/app/components/TeamCard.tsx index ad771bf..a3998dd 100644 --- a/src/app/components/TeamCard.tsx +++ b/src/app/components/TeamCard.tsx @@ -5,7 +5,6 @@ import { useState } from 'react' import { useRouter } from 'next/navigation' import Button from './Button' import TeamPremierRankBadge from './TeamPremierRankBadge' -import { useLiveTeam } from '../hooks/useLiveTeam' import type { Team, Player } from '../types/team' import LoadingSpinner from './LoadingSpinner' @@ -27,15 +26,6 @@ export default function TeamCard({ const router = useRouter() const [joining, setJoining] = useState(false) - /* ---------- Live-Daten ---------- */ - const data = useLiveTeam(team) - if (!data) return - - const players: Player[] = [ - ...(data.activePlayers ?? []), - ...(data.inactivePlayers ?? []), - ] - /* ---------- Join / Reject ---------- */ const isRequested = Boolean(invitationId) const isDisabled = joining || currentUserSteamId === data.leader diff --git a/src/app/components/TeamCardComponent.tsx b/src/app/components/TeamCardComponent.tsx index b1784f7..dda2e6a 100644 --- a/src/app/components/TeamCardComponent.tsx +++ b/src/app/components/TeamCardComponent.tsx @@ -13,7 +13,7 @@ import { acceptInvitation, rejectInvitation, markOneAsRead -} from '@/app/lib/team-actions' +} from '@/app/lib/sse-actions' import { Player, Team } from '../types/team' type Props = { diff --git a/src/app/components/TeamMemberView.tsx b/src/app/components/TeamMemberView.tsx index bff0209..cae0bc2 100644 --- a/src/app/components/TeamMemberView.tsx +++ b/src/app/components/TeamMemberView.tsx @@ -13,24 +13,21 @@ import InvitePlayersModal from './InvitePlayersModal' import Modal from './Modal' import { Player, Team } from '../types/team' import { useSession } from 'next-auth/react' -import { useSSE } from '@/app/lib/useSSEStore' import { AnimatePresence, motion } from 'framer-motion' import { leaveTeam, reloadTeam, renameTeam, revokeInvitation, -} from '@/app/lib/team-actions' +} from '@/app/lib/sse-actions' import Button from './Button' import Image from 'next/image' import TeamPremierRankBadge from './TeamPremierRankBadge' import Link from 'next/link' -import { useWebSocketListener } from '../hooks/useWebSocketListener' +import { useTeamStore } from '../lib/stores' type Props = { - team: Team | null - activePlayers: Player[] - inactivePlayers: Player[] + team: Team activeDragItem: Player | null isDragging: boolean showLeaveModal: boolean @@ -40,8 +37,6 @@ type Props = { setShowInviteModal: (v: boolean) => void setActiveDragItem: (item: Player | null) => void setIsDragging: (v: boolean) => void - setactivePlayers: (players: Player[]) => void - setInactivePlayers: (players: Player[]) => void adminMode?: boolean } @@ -49,8 +44,6 @@ type InvitedPlayer = Player & { invitationId: string } export default function TeamMemberView({ team, - activePlayers, - inactivePlayers, activeDragItem, isDragging, showLeaveModal, @@ -59,47 +52,45 @@ export default function TeamMemberView({ setShowInviteModal, setActiveDragItem, setIsDragging, - setactivePlayers, - setInactivePlayers, adminMode = false, }: Props) { + const { data: session } = useSession() - const { source } = useSSE() - const [kickCandidate, setKickCandidate] = useState(null) - const [promoteCandidate, setPromoteCandidate] = useState(null) - const currentUserSteamId = session?.user?.steamId || '' const isLeader = currentUserSteamId === team?.leader const canManage = adminMode || isLeader const canInvite = isLeader && !adminMode const canAddDirect = adminMode - const [showRenameModal, setShowRenameModal] = useState(false) + + const [activePlayers, setActivePlayers] = useState([]) + const [inactivePlayers, setInactivePlayers] = 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 [isEditingLogo, setIsEditingLogo] = useState(false) - const [logoPreview, setLogoPreview] = useState(null) - const [logoFile, setLogoFile] = useState(null) const [teamState, setTeamState] = useState(team) const [saveSuccess, setSaveSuccess] = useState(false) const [invitedPlayers, setInvitedPlayers] = useState([]) - useWebSocketListener('ws-team-update', () => console.log("yeah?")) + console.log('[TeamMemberView] team from store:', team) + console.log('[TeamMemberView] teamState:', teamState) + console.log('[TeamMemberView] activePlayers:', activePlayers) + console.log('[TeamMemberView] inactivePlayers:', inactivePlayers) useEffect(() => { + if (!team || !team.id) return + setTeamState(team) - }, [team]) + setEditedName(team.name || '') + setActivePlayers(team.activePlayers) + setInactivePlayers(team.inactivePlayers) - useEffect(() => { - if (team?.invitedPlayers?.length) { - const unique = Array.from( - new Map(team.invitedPlayers.map(p => [p.steamId, p])).values() - ) - setInvitedPlayers(unique.sort((a, b) => a.name.localeCompare(b.name))) - } else { - setInvitedPlayers([]) - } - }, [team]) + const uniqueInvites = Array.from( + new Map((team.invitedPlayers ?? []).map(p => [p.steamId, p])).values() + ) + setInvitedPlayers(uniqueInvites.sort((a, b) => a.name.localeCompare(b.name))) + }, [team?.id]) const handleDragStart = (event: any) => { const id = event.active.id @@ -163,7 +154,7 @@ export default function TeamMemberView({ newActive.sort((a, b) => a.name.localeCompare(b.name)) newInactive.sort((a, b) => a.name.localeCompare(b.name)) - setactivePlayers(newActive) + setActivePlayers(newActive) setInactivePlayers(newInactive) await updateTeamMembers(team!.id, newActive, newInactive) setSaveSuccess(true) @@ -176,7 +167,7 @@ export default function TeamMemberView({ if (!updated) return setTeamState(updated) - setactivePlayers(updated.activePlayers) + setActivePlayers(updated.activePlayers) setInactivePlayers(updated.inactivePlayers) setInvitedPlayers(updated.invitedPlayers) } @@ -187,7 +178,7 @@ export default function TeamMemberView({ const newActive = activePlayers.filter(p => p.steamId !== kickCandidate.steamId) const newInactive = inactivePlayers.filter(p => p.steamId !== kickCandidate.steamId) - setactivePlayers(newActive) + setActivePlayers(newActive) setInactivePlayers(newInactive) await fetch('/api/team/kick', { @@ -265,410 +256,410 @@ export default function TeamMemberView({ ) return ( -
-
-
- {/* Teamlogo mit Fallback */} -
-
canManage && document.getElementById('logoUpload')?.click()} - > - Teamlogo +
+
+
+ {/* Teamlogo mit Fallback */} +
+
canManage && document.getElementById('logoUpload')?.click()} + > + Teamlogo - {/* Overlay beim Hover */} - {canManage && ( -
- - - -
- )} -
- - {/* Hidden file input */} + {/* Overlay beim Hover */} {canManage && ( - { - const file = e.target.files?.[0] - if (!file) return - - const formData = new FormData() - formData.append('logo', file) - formData.append('teamId', teamState.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.') - } - }} - /> +
+ + + +
)}
- {/* Teamname + Bearbeiten */} -
- {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" - /> + {/* Hidden file input */} + {canManage && ( + { + const file = e.target.files?.[0] + if (!file) return - {/* ✔ Übernehmen */} + const formData = new FormData() + formData.append('logo', file) + formData.append('teamId', teamState.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.') + } + }} + /> + )} +
+ + {/* Teamname + Bearbeiten */} +
+ {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" + /> + + {/* ✔ Übernehmen */} + + + {/* ✖ Abbrechen */} + + + ) : ( + <> +
+

+ {teamState.name ?? 'Team'} +

+ +
+ {canManage && ( - - {/* ✖ Abbrechen */} - - - ) : ( - <> -
-

- {teamState.name ?? 'Team'} -

- -
- {canManage && ( - - )} - - )} -
-
- - {/* Aktionen */} -
- {canManage && ( - + )} + )} -
- - -
- - p.steamId)} strategy={verticalListSortingStrategy}> - {renderMemberList(activePlayers)} - - - - - p.steamId)} strategy={verticalListSortingStrategy}> - {renderMemberList(inactivePlayers)} - {canManage && ( - - { - setShowInviteModal(false) - setTimeout(() => setShowInviteModal(true), 0) - }} - > -
- - - -
-
-
- )} -
-
+ + {/* Aktionen */} +
+ {canManage && ( + + )} + +
+
+ + +
+ + p.steamId)} strategy={verticalListSortingStrategy}> + {renderMemberList(activePlayers)} + + + + + 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={teamState.leader} - isSelectable={false} - isInvite={true} - rank={player.premierRank} - onKick={revokeInvitation} - invitationId={player.invitationId} - /> - - ))} - -
+ {invitedPlayers.length > 0 && ( +
+
+

Eingeladene Spieler

+
+
+
+ + {invitedPlayers.map((player: InvitedPlayer) => ( + + {}} + draggable={false} + currentUserSteamId={currentUserSteamId} + teamLeaderSteamId={teamState.leader} + isSelectable={false} + isInvite={true} + rank={player.premierRank} + onKick={revokeInvitation} + invitationId={player.invitationId} + /> + + ))} +
- )} -
- - - {activeDragItem && ( - - )} - - +
+ )} +
+ + + {activeDragItem && ( + + )} + + - {/* Modal(s) */} - {canInvite && ( - setShowInviteModal(false)} - onSuccess={() => {}} - team={teamState} - /> - )} - - {canAddDirect && ( - setShowInviteModal(false)} - onSuccess={() => {}} - team={teamState} - directAdd - /> - )} - - {/* Leader-spezifische Modale (z. B. Team verlassen) */} - {isLeader && ( - setShowLeaveModal(false)} - onSuccess={() => setShowLeaveModal(false)} - team={teamState} + {/* Modal(s) */} + {canInvite && ( + setShowInviteModal(false)} + onSuccess={() => {}} + team={teamState} /> - )} - + )} + + {canAddDirect && ( + setShowInviteModal(false)} + onSuccess={() => {}} + team={teamState} + directAdd + /> + )} - {canManage && promoteCandidate && ( - setPromoteCandidate(null)} - onSave={async () => { - await promoteToLeader(promoteCandidate.steamId) - setPromoteCandidate(null) - }} - closeButtonTitle="Übertragen" - closeButtonColor="blue" - > - {/* ► PlayerCard des Kandidaten */} -
- {}} - draggable={false} - currentUserSteamId={currentUserSteamId} - teamLeaderSteamId={teamState.leader} - hideActions - isSelectable={false} - /> -
+ {/* Leader-spezifische Modale (z. B. Team verlassen) */} + {isLeader && ( + setShowLeaveModal(false)} + onSuccess={() => setShowLeaveModal(false)} + team={teamState} + /> + )} + -

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

-
- )} - - {canManage && kickCandidate && ( - setKickCandidate(null)} - onSave={confirmKick} - closeButtonTitle="Entfernen" - closeButtonColor="red" - > - {/* ► PlayerCard des Kandidaten */} -
- {}} - draggable={false} - currentUserSteamId={currentUserSteamId} - teamLeaderSteamId={teamState.leader} - hideActions - isSelectable={false} - /> -
+ {canManage && promoteCandidate && ( + setPromoteCandidate(null)} + onSave={async () => { + await promoteToLeader(promoteCandidate.steamId) + setPromoteCandidate(null) + }} + closeButtonTitle="Übertragen" + closeButtonColor="blue" + > + {/* ► PlayerCard des Kandidaten */} +
+ {}} + draggable={false} + currentUserSteamId={currentUserSteamId} + teamLeaderSteamId={teamState.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: teamState.id }), - }) - setShowDeleteModal(false) - window.location.href = '/' - }} - 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. -

-
- )} -
+

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

+ + )} + + {canManage && kickCandidate && ( + setKickCandidate(null)} + onSave={confirmKick} + closeButtonTitle="Entfernen" + closeButtonColor="red" + > + {/* ► PlayerCard des Kandidaten */} +
+ {}} + draggable={false} + currentUserSteamId={currentUserSteamId} + teamLeaderSteamId={teamState.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: teamState.id }), + }) + setShowDeleteModal(false) + window.location.href = '/' + }} + 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. +

+
+ )} +
) } diff --git a/src/app/hooks/useLiveTeam.tsx b/src/app/hooks/useLiveTeam.tsx deleted file mode 100644 index 31a50d8..0000000 --- a/src/app/hooks/useLiveTeam.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import type { Team, Player } from '../types/team' - -const events = [ - 'ws-team-renamed', - 'ws-team-member-joined', - 'ws-team-member-left', - 'ws-team-kick', - 'ws-team-kick-other', - 'ws-team-leader-changed', - 'ws-team-logo-updated', -] - -export function useLiveTeam(initialTeam: Team) { - /** Der lokale Zustand hat dieselbe Form wie `Team` */ - const [team, setTeam] = useState(initialTeam) - - useEffect(() => { - const refresh = async () => { - const res = await fetch(`/api/team/get?id=${initialTeam.id}`) - if (!res.ok) return - const { team: t } = await res.json() - if (!t) return - - setTeam({ - id : t.id, - name : t.teamname, - logo : t.logo, - leader: t.leader, - activePlayers : t.activePlayers ?? [], - inactivePlayers: t.inactivePlayers ?? [], - }) - } - - const handler = (e: Event) => { - const ev = e as CustomEvent - if (ev.detail?.teamId === initialTeam.id) refresh() - } - - events.forEach(evt => window.addEventListener(evt, handler)) - return () => events.forEach(evt => window.removeEventListener(evt, handler)) - }, [initialTeam.id]) - - return team -} diff --git a/src/app/hooks/useTeamManager.tsx b/src/app/hooks/useTeamManager.tsx deleted file mode 100644 index cb21380..0000000 --- a/src/app/hooks/useTeamManager.tsx +++ /dev/null @@ -1,332 +0,0 @@ -// useTeamManager.tsx - -import { useEffect, useState, useImperativeHandle } from 'react' -import { Player, Team } from '../types/team' -import { useSession } from 'next-auth/react' -import { useWebSocketListener } from '@/app/hooks/useWebSocketListener' - -export type Invitation = { - id: string - teamId: string - teamName: string - type?: 'team-invite' | 'team-join-request' // 👈 hinzufügen -} - -export function useTeamManager( - props: { refetchKey?: string; teamId?: string }, - ref: React.Ref -) { - const [team, setTeam] = useState(null) - const [activePlayers, setactivePlayers] = useState([]) - const [inactivePlayers, setInactivePlayers] = useState([]) - const [showLeaveModal, setShowLeaveModal] = useState(false) - const [showInviteModal, setShowInviteModal] = useState(false) - const [activeDragItem, setActiveDragItem] = useState(null) - const [isDragging, setIsDragging] = useState(false) - const [isLoading, setIsLoading] = useState(true) - const [pendingInvitation, setPendingInvitation] = useState(null) - - const { data: session } = useSession() - - const fetchTeam = async () => { - setIsLoading(true) - try { - const url = props.teamId - ? `/api/team/${encodeURIComponent(props.teamId)}` - : '/api/team' - - const res = await fetch(url) - - if (res.status === 404) { - setTeam(null) - setactivePlayers([]) - setInactivePlayers([]) - return - } - - if (!res.ok) throw new Error('Fehler beim Abrufen des Teams') - - const data = await res.json() - - const teamData = data.team ?? data - - if (!teamData || !teamData.id) { - setTeam(null) - setactivePlayers([]) - setInactivePlayers([]) - return - } - - // ── 1. evtl. nur Steam-IDs? ─────────────────────────────── - let newActive = teamData.activePlayers ?? [] - let newInactive = teamData.inactivePlayers ?? [] - - const playersAreStrings = - typeof newActive[0] === 'string' || typeof newInactive[0] === 'string' - - if (playersAreStrings && (newActive.length || newInactive.length)) { - /* Alle IDs sammeln und User-Infos nachladen */ - const steamIds = [...newActive, ...newInactive] - const resUsers = await fetch('/api/user/list', { - method : 'POST', - headers: { 'Content-Type': 'application/json' }, - body : JSON.stringify({ steamIds }) - }) - - - const json = await resUsers.json() - const users: Player[] = Array.isArray(json) ? json - : Array.isArray(json.users) ? json.users - : [] - - const map = Object.fromEntries(users.map(u => [u.steamId, u])) - - newActive = newActive - .map((id: string) => map[id]) - .filter(Boolean) - .sort((a: Player, b: Player) => a.name.localeCompare(b.name)) - newInactive = newInactive - .map((id: string) => map[id]) - .filter(Boolean) - .sort((a: Player, b: Player) => a.name.localeCompare(b.name)) - } else { - newActive = newActive .sort((a: Player, b: Player) => a.name.localeCompare(b.name)) - newInactive = newInactive.sort((a: Player, b: Player) => a.name.localeCompare(b.name)) - } - - setTeam({ - id: teamData.id, - name: teamData.name, - leader: teamData.leader, - logo: teamData.logo, - activePlayers : newActive, - inactivePlayers: newInactive, - invitedPlayers : teamData.invitedPlayers ?? [], - }) - setactivePlayers(newActive) - setInactivePlayers(newInactive) - } catch (error) { - console.error('Fehler beim Laden des Teams:', error) - setTeam(null) - } finally { - setIsLoading(false) - } - } - - const fetchInvitations = async () => { - try { - const res = await fetch('/api/user/invitations') - if (res.ok) { - const data = await res.json() - const invitations = (data.invitations || []) as Invitation[] - - // Nur "team-invite" berücksichtigen - const invite = invitations.find(i => i.type === 'team-invite') - setPendingInvitation(invite || null) - } - } catch (error) { - console.error('Fehler beim Laden der Einladungen:', error) - } - } - - useEffect(() => { - const load = async () => { - if (props.teamId) // 👉 Admin-Detail: nur Team holen - await fetchTeam() - else // 👉 eigener User: Team + Einladungen - await Promise.all([fetchTeam(), fetchInvitations()]) - } - load() - }, [props.refetchKey, props.teamId]) // teamId als Dep nicht vergessen - - useWebSocketListener('ws-invitation', fetchInvitations) - useWebSocketListener('ws-team-invite', fetchInvitations) - useWebSocketListener('ws-team-invite-reject', fetchInvitations) - useWebSocketListener('ws-team-update', fetchTeam) - useWebSocketListener('ws-team-kick', () => { - fetchTeam() - fetchInvitations() - }) - useWebSocketListener('ws-team-kick-other', fetchTeam) - useWebSocketListener('ws-team-joined', fetchTeam) - useWebSocketListener('ws-team-member-joined', fetchTeam) - useWebSocketListener('ws-team-left', () => { - fetchTeam() - fetchInvitations() - }) - useWebSocketListener('ws-team-member-left', fetchTeam) - useWebSocketListener('ws-team-leader-changed', fetchTeam) - useWebSocketListener('ws-team-join-request', fetchInvitations) - useWebSocketListener('ws-team-renamed', fetchTeam) - - - const reloadTeam = async () => { - await fetchTeam() - await fetchInvitations() - } - - useImperativeHandle(ref, () => ({ reloadTeam })) - - const acceptInvitation = async (invitationId?: string) => { - const id = invitationId || pendingInvitation?.id - if (!id) return - try { - await fetch('/api/user/invitations/accept', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ invitationId: id }), - }) - if (pendingInvitation?.id === id) setPendingInvitation(null) - await fetchTeam() - } catch (error) { - console.error('Fehler beim Annehmen der Einladung:', error) - } - } - - const rejectInvitation = async (invitationId?: string) => { - const id = invitationId || pendingInvitation?.id - if (!id) return - try { - await fetch('/api/user/invitations/reject', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ invitationId: id }), - }) - if (pendingInvitation?.id === id) setPendingInvitation(null) - } catch (error) { - console.error('Fehler beim Ablehnen der Einladung:', error) - } - } - - const revokeInvitation = async (invitationId: string) => { - const id = invitationId || pendingInvitation?.id - if (!id) return - try { - await fetch('/api/user/invitations/revoke', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ invitationId: id }), - }) - if (pendingInvitation?.id === id) setPendingInvitation(null) - await fetchTeam() - } catch (error) { - console.error('Fehler beim Annehmen der Einladung:', error) - } - } - - const markAllAsRead = async () => { - try { - await fetch('/api/notifications/mark-all-read', { method: 'POST' }) - } catch (err) { - console.error('Fehler beim Markieren aller Benachrichtigungen:', err) - } - } - - const markOneAsRead = async (id: string) => { - try { - await fetch(`/api/notifications/mark-read/${id}`, { method: 'POST' }) - } catch (err) { - console.error(`Fehler beim Markieren von Benachrichtigung ${id}:`, err) - } - } - - const handleInviteAction = async (action: 'accept' | 'reject', invitationId: string) => { - try { - const res = await fetch(`/api/user/invitations/${action}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ invitationId }), - }) - - // 💡 Einladung war schon gelöscht - if (res.status === 404 && action === 'accept') { - console.warn('Einladung wurde bereits entfernt.') - setPendingInvitation(null) - await fetchTeam() - return - } - - if (!res.ok) throw new Error('Aktion fehlgeschlagen') - - if (action === 'accept') await fetchTeam() - } catch (err) { - console.error(`[${action}] Fehler beim Ausführen:`, err) - } - } - - - const leaveTeam = async (steamId: string, newLeaderId?: string): Promise => { - try { - const payload = newLeaderId ? { steamId, newLeaderId } : { steamId } - const res = await fetch('/api/team/leave', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }) - - if (!res.ok) { - const error = await res.json() - console.error('Fehler beim Verlassen:', error.message) - return false - } - - await fetchTeam() - return true - } catch (err) { - console.error('Fehler beim Verlassen des Teams:', err) - return false - } - } - - const renameTeam = async (teamId: string, newName: string) => { - try { - await fetch('/api/team/rename', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ teamId, newName }), - }) - } catch (err) { - console.error('Fehler beim Umbenennen:', err) - } - } - - const deleteTeam = async (teamId: string) => { - try { - await fetch('/api/team/delete', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ teamId }), - }) - } catch (err) { - console.error('Fehler beim Löschen:', err) - } - } - - return { - team, - activePlayers, - inactivePlayers, - activeDragItem, - isDragging, - isLoading, - showLeaveModal, - showInviteModal, - pendingInvitation, - setShowLeaveModal, - setShowInviteModal, - setActiveDragItem, - setIsDragging, - setactivePlayers, - setInactivePlayers, - acceptInvitation, - rejectInvitation, - revokeInvitation, - markAllAsRead, - markOneAsRead, - handleInviteAction, - leaveTeam, - reloadTeam, - renameTeam, - deleteTeam, - } -} diff --git a/src/app/hooks/useWebSocketListener.ts b/src/app/hooks/useWebSocketListener.ts deleted file mode 100644 index 3c70cb2..0000000 --- a/src/app/hooks/useWebSocketListener.ts +++ /dev/null @@ -1,21 +0,0 @@ -'use client' - -import { useEffect } from 'react' - -export function useWebSocketListener( - eventName: string, - handler: (data: T) => void -) { - useEffect(() => { - const listener = (event: Event) => { - if (!(event instanceof CustomEvent)) return - handler(event.detail) - } - - window.addEventListener(eventName, listener as EventListener) - - return () => { - window.removeEventListener(eventName, listener as EventListener) - } - }, [eventName, handler]) -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b395391..0ad0ff2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,7 +8,7 @@ import ThemeProvider from "@/theme/theme-provider"; import Script from "next/script"; import NotificationCenter from './components/NotificationCenter' import Navbar from "./components/Navbar"; -import SSEListener from "./components/SSEListener"; +import SSEHandler from "./lib/SSEHandler"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -40,7 +40,7 @@ export default function RootLayout({ disableTransitionOnChange > - + {/* Sidebar und Content direkt nebeneinander */} {children} diff --git a/src/app/lib/SSEHandler.tsx b/src/app/lib/SSEHandler.tsx new file mode 100644 index 0000000..f7f639b --- /dev/null +++ b/src/app/lib/SSEHandler.tsx @@ -0,0 +1,56 @@ +'use client' + +import { useEffect } from 'react' +import { useSSEStore } from '@/app/lib/useSSEStore' +import { useSession } from 'next-auth/react' +import { reloadTeam } from '@/app/lib/sse-actions' +import { useRouter } from 'next/navigation' +import { useTeamStore } from '@/app/lib/stores' + +export default function SSEHandler() { + const { data: session } = useSession() + const steamId = session?.user?.steamId + const router = useRouter() + const { setTeam } = useTeamStore() + + const { connect, lastEvent } = useSSEStore() + + useEffect(() => { + if (steamId) connect(steamId) + }, [steamId]) + + useEffect(() => { + if (!lastEvent) return + + const { type, payload } = lastEvent + + const handleEvent = async () => { + switch (type) { + case 'team-updated': + if (payload.teamId) { + const updated = await reloadTeam(payload.teamId) + if (updated) { + setTeam(updated) // ✅ zentral setzen + } + } + break + + case 'team-kick-other': + console.log('[SSE] Du wurdest gekickt') + router.push('/') + break + + case 'team-left': + console.log('[SSE] Jemand hat das Team verlassen') + break + + default: + console.log('[SSE] Unbehandeltes Event:', type, payload) + } + } + + handleEvent() + }, [lastEvent]) + + return null // kein UI +} diff --git a/src/app/lib/team-actions.ts b/src/app/lib/sse-actions.ts similarity index 96% rename from src/app/lib/team-actions.ts rename to src/app/lib/sse-actions.ts index 3526da9..744bb76 100644 --- a/src/app/lib/team-actions.ts +++ b/src/app/lib/sse-actions.ts @@ -1,10 +1,11 @@ -// lib/team-actions.ts +// lib/sse-actions.ts -import { Player, Team } from '../types/team' +import { Player, Team, InvitedPlayer } from '../types/team' // 🔄 Team laden export async function reloadTeam(teamId: string): Promise { try { + console.log("reloadTeam"); const res = await fetch(`/api/team/${encodeURIComponent(teamId)}`) if (!res.ok) throw new Error('Fehler beim Abrufen des Teams') @@ -12,7 +13,7 @@ export async function reloadTeam(teamId: string): Promise { const team = data.team ?? data if (!team) return null - const sortByName = (players: Player[]) => + const sortByName = (players: T[]): T[] => players.sort((a, b) => a.name.localeCompare(b.name)) return { diff --git a/src/app/lib/sse-server-client.ts b/src/app/lib/sse-server-client.ts index 7d8654f..38c52a0 100644 --- a/src/app/lib/sse-server-client.ts +++ b/src/app/lib/sse-server-client.ts @@ -4,6 +4,7 @@ const host = 'localhost' export async function sendServerSSEMessage(message: any) { try { + console.log("Sending message: ", message); await fetch(`http://${host}:3001/send`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/src/app/lib/stores.ts b/src/app/lib/stores.ts new file mode 100644 index 0000000..c129012 --- /dev/null +++ b/src/app/lib/stores.ts @@ -0,0 +1,15 @@ +// lib/stores.ts +'use client' + +import { create } from 'zustand' +import { Team } from '../types/team' + +type TeamState = { + team: Team | null + setTeam: (t: Team) => void +} + +export const useTeamStore = create((set) => ({ + team: null, + setTeam: (team) => set({ team }), +})) diff --git a/src/app/lib/useSSEStore.ts b/src/app/lib/useSSEStore.ts index a9506f6..b44e001 100644 --- a/src/app/lib/useSSEStore.ts +++ b/src/app/lib/useSSEStore.ts @@ -1,51 +1,56 @@ +// useSSEStore.ts import { create } from 'zustand' +type SSEEvent = { + type: string + payload: any +} + type SSEState = { source: EventSource | null isConnected: boolean - connect: (steamId: string) => EventSource | undefined + lastEvent: SSEEvent | null + connect: (steamId: string) => void disconnect: () => void } -export const useSSE = create((set, get) => { +export const useSSEStore = create((set, get) => { let reconnectTimeout: NodeJS.Timeout | null = null - const connect = (steamId: string): EventSource | undefined => { - const current = get().source - if (current) return current + const connect = (steamId: string) => { + if (get().source) return const source = new EventSource(`http://localhost:3001/events?steamId=${steamId}`) - source.onopen = () => { - console.log('[SSE] Verbunden!') - set({ source, isConnected: true }) - } + const eventTypes = [ + 'team-updated', + 'team-leader-changed', + 'team-member-joined', + 'team-member-left', + 'team-kick', + 'team-kick-other', + 'team-left', + 'team-renamed', + 'team-logo-updated', + // ... beliebig erweiterbar + ] - source.onmessage = (event) => { - console.log('[SSE] Nachricht:', event.data) - try { - const data = JSON.parse(event.data) + for (const type of eventTypes) { + source.addEventListener(type, (event) => { + try { + const data = JSON.parse((event as MessageEvent).data) + console.log(`[SSE] ${type}:`, data) - // Zentrale Weiterleitung aller Events, die ein "type" haben - if (data?.type) { - window.dispatchEvent(new CustomEvent(`sse-${data.type}`, { detail: data })) + set({ lastEvent: { type, payload: data } }) + + } catch (err) { + console.error(`[SSE] Fehler beim Parsen von ${type}:`, err) } - } catch (err) { - console.error('[SSE] Ungültige Nachricht:', event.data) - } + }) } - source.addEventListener('notification', (event) => { - try { - const data = JSON.parse((event as MessageEvent).data) - window.dispatchEvent(new CustomEvent(`sse-${data.type}`, { detail: data })) - } catch (err) { - console.error('[SSE] Ungültige Nachricht:', event) - } - }) - source.onerror = (err) => { - console.warn('[SSE] Verbindung verloren, versuche Reconnect...') + console.warn('[SSE] Fehler, versuche Reconnect...') source.close() set({ source: null, isConnected: false }) @@ -57,25 +62,24 @@ export const useSSE = create((set, get) => { } } + source.onopen = () => { + console.log('[SSE] Verbunden!') + set({ source, isConnected: true }) + } + set({ source }) - return source // ✅ wichtig } const disconnect = () => { - const source = get().source - if (source) { - source.close() - } - if (reconnectTimeout) { - clearTimeout(reconnectTimeout) - reconnectTimeout = null - } + get().source?.close() + if (reconnectTimeout) clearTimeout(reconnectTimeout) set({ source: null, isConnected: false }) } return { source: null, isConnected: false, + lastEvent: null, connect, disconnect, }