changes live-actions

This commit is contained in:
Linrador 2025-08-06 23:25:17 +02:00
parent 404e577983
commit 903d898a0a
17 changed files with 562 additions and 970 deletions

View File

@ -1,30 +1,48 @@
// ───────────────────────────────────────────────────────────
// src/app/(admin)/admin/teams/[teamId]/TeamAdminClient.tsx
// ───────────────────────────────────────────────────────────
'use client' 'use client'
import { useState } from 'react' import { useEffect, useState } from 'react'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import LoadingSpinner from '@/app/components/LoadingSpinner' import LoadingSpinner from '@/app/components/LoadingSpinner'
import TeamMemberView from '@/app/components/TeamMemberView' import TeamMemberView from '@/app/components/TeamMemberView'
//import { useTeamManager } from '@/app/hooks/useTeamManager' import { useTeamStore } from '@/app/lib/stores'
import { reloadTeam } from '@/app/lib/sse-actions'
export default function TeamAdminClient({ teamId }: { teamId: string }) { export default function TeamAdminClient({ teamId }: { teamId: string }) {
const [refetchKey, setRefetchKey] = useState<string>() const [loading, setLoading] = useState(true)
const { data: session } = useSession() const { data: session } = useSession()
const { team, setTeam } = useTeamStore()
// jetzt wird die ID korrekt übergeben ➜ /api/team/[id] useEffect(() => {
//const teamManager = useTeamManager({ teamId, refetchKey }, null) const fetch = async () => {
const result = await reloadTeam(teamId)
console.log('[TeamAdminClient] reloadTeam returned:', result)
if (result) {
setTeam(result)
}
setLoading(false)
}
if (teamManager.isLoading) return <LoadingSpinner /> if (teamId) fetch()
}, [teamId, setTeam])
if (loading || !team) {
return <LoadingSpinner />
}
return ( return (
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">
<TeamMemberView <TeamMemberView
{...teamManager} team={team}
currentUserSteamId={session?.user?.steamId ?? ''} currentUserSteamId={session?.user?.steamId ?? ''}
adminMode={true} adminMode={true}
activeDragItem={null}
isDragging={false}
showLeaveModal={false}
showInviteModal={false}
setShowLeaveModal={() => {}}
setShowInviteModal={() => {}}
setActiveDragItem={() => {}}
setIsDragging={() => {}}
/> />
</div> </div>
) )

View File

@ -5,7 +5,7 @@ import Modal from './Modal'
import MiniCard from './MiniCard' import MiniCard from './MiniCard'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import { Player, Team } from '../types/team' import { Player, Team } from '../types/team'
import { leaveTeam } from '../lib/team-actions' import { leaveTeam } from '../lib/sse-actions'
type Props = { type Props = {
show: boolean show: boolean

View File

@ -4,7 +4,7 @@
import Button from './Button' import Button from './Button'
import Image from 'next/image' import Image from 'next/image'
import PremierRankBadge from './PremierRankBadge' import PremierRankBadge from './PremierRankBadge'
import { revokeInvitation } from '../lib/team-actions' import { revokeInvitation } from '../lib/sse-actions'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
type MiniCardProps = { type MiniCardProps = {

View File

@ -2,9 +2,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import NotificationDropdown from './NotificationDropdown' import NotificationDropdown from './NotificationDropdown'
import { useSSE } from '@/app/lib/useSSEStore'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import { useTeamManager } from '../hooks/useTeamManager'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
/* ────────────────────────────────────────────────────────── */ /* ────────────────────────────────────────────────────────── */
@ -27,7 +25,6 @@ export default function NotificationCenter() {
const { data: session } = useSession() const { data: session } = useSession()
const [notifications, setNotifications] = useState<Notification[]>([]) const [notifications, setNotifications] = useState<Notification[]>([])
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const { source, connect } = useSSE()
//const { markAllAsRead, markOneAsRead, handleInviteAction } = useTeamManager({}, null) //const { markAllAsRead, markOneAsRead, handleInviteAction } = useTeamManager({}, null)
const router = useRouter() const router = useRouter()
const [previewText, setPreviewText] = useState<string | null>(null) const [previewText, setPreviewText] = useState<string | null>(null)
@ -70,12 +67,12 @@ export default function NotificationCenter() {
} }
loadNotifications() loadNotifications()
connect(steamId) // SSE starten }, [session?.user?.steamId])
}, [session?.user?.steamId, connect])
/* --- Live-Updates über SSE empfangen -------------------- */ /* --- Live-Updates über SSE empfangen -------------------- */
useEffect(() => { useEffect(() => {
if (!source) return return;
//if (!source) return
/* Handler für JEDES eintreffende Paket ------------------ */ /* Handler für JEDES eintreffende Paket ------------------ */
const handleEvent = (event: MessageEvent) => { const handleEvent = (event: MessageEvent) => {
@ -129,17 +126,17 @@ export default function NotificationCenter() {
] ]
/* Named Events abonnieren ------------------------------ */ /* Named Events abonnieren ------------------------------ */
eventNames.forEach(evt => source.addEventListener(evt, handleEvent)) //eventNames.forEach(evt => source.addEventListener(evt, handleEvent))
/* Fallback: Server sendet evtl. Events ohne „event:“----- */ /* Fallback: Server sendet evtl. Events ohne „event:“----- */
source.onmessage = handleEvent //source.onmessage = handleEvent
/* Aufräumen bei Unmount -------------------------------- */ /* Aufräumen bei Unmount -------------------------------- */
return () => { return () => {
eventNames.forEach(evt => source.removeEventListener(evt, handleEvent)) //eventNames.forEach(evt => source.removeEventListener(evt, handleEvent))
source.onmessage = null //source.onmessage = null
} }
}, [source]) }, /*[source] */)
/* ────────────────────────────────────────────────────────── */ /* ────────────────────────────────────────────────────────── */
/* Render */ /* Render */

View File

@ -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
}

View File

@ -5,7 +5,6 @@ import { useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import Button from './Button' import Button from './Button'
import TeamPremierRankBadge from './TeamPremierRankBadge' import TeamPremierRankBadge from './TeamPremierRankBadge'
import { useLiveTeam } from '../hooks/useLiveTeam'
import type { Team, Player } from '../types/team' import type { Team, Player } from '../types/team'
import LoadingSpinner from './LoadingSpinner' import LoadingSpinner from './LoadingSpinner'
@ -27,15 +26,6 @@ export default function TeamCard({
const router = useRouter() const router = useRouter()
const [joining, setJoining] = useState(false) const [joining, setJoining] = useState(false)
/* ---------- Live-Daten ---------- */
const data = useLiveTeam(team)
if (!data) return <LoadingSpinner />
const players: Player[] = [
...(data.activePlayers ?? []),
...(data.inactivePlayers ?? []),
]
/* ---------- Join / Reject ---------- */ /* ---------- Join / Reject ---------- */
const isRequested = Boolean(invitationId) const isRequested = Boolean(invitationId)
const isDisabled = joining || currentUserSteamId === data.leader const isDisabled = joining || currentUserSteamId === data.leader

View File

@ -13,7 +13,7 @@ import {
acceptInvitation, acceptInvitation,
rejectInvitation, rejectInvitation,
markOneAsRead markOneAsRead
} from '@/app/lib/team-actions' } from '@/app/lib/sse-actions'
import { Player, Team } from '../types/team' import { Player, Team } from '../types/team'
type Props = { type Props = {

View File

@ -13,24 +13,21 @@ import InvitePlayersModal from './InvitePlayersModal'
import Modal from './Modal' import Modal from './Modal'
import { Player, Team } from '../types/team' import { Player, Team } from '../types/team'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import { useSSE } from '@/app/lib/useSSEStore'
import { AnimatePresence, motion } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion'
import { import {
leaveTeam, leaveTeam,
reloadTeam, reloadTeam,
renameTeam, renameTeam,
revokeInvitation, revokeInvitation,
} from '@/app/lib/team-actions' } 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'
import Link from 'next/link' import Link from 'next/link'
import { useWebSocketListener } from '../hooks/useWebSocketListener' import { useTeamStore } from '../lib/stores'
type Props = { type Props = {
team: Team | null team: Team
activePlayers: Player[]
inactivePlayers: Player[]
activeDragItem: Player | null activeDragItem: Player | null
isDragging: boolean isDragging: boolean
showLeaveModal: boolean showLeaveModal: boolean
@ -40,8 +37,6 @@ type Props = {
setShowInviteModal: (v: boolean) => void setShowInviteModal: (v: boolean) => void
setActiveDragItem: (item: Player | null) => void setActiveDragItem: (item: Player | null) => void
setIsDragging: (v: boolean) => void setIsDragging: (v: boolean) => void
setactivePlayers: (players: Player[]) => void
setInactivePlayers: (players: Player[]) => void
adminMode?: boolean adminMode?: boolean
} }
@ -49,8 +44,6 @@ type InvitedPlayer = Player & { invitationId: string }
export default function TeamMemberView({ export default function TeamMemberView({
team, team,
activePlayers,
inactivePlayers,
activeDragItem, activeDragItem,
isDragging, isDragging,
showLeaveModal, showLeaveModal,
@ -59,47 +52,45 @@ export default function TeamMemberView({
setShowInviteModal, setShowInviteModal,
setActiveDragItem, setActiveDragItem,
setIsDragging, setIsDragging,
setactivePlayers,
setInactivePlayers,
adminMode = false, adminMode = false,
}: Props) { }: Props) {
const { data: session } = useSession()
const { source } = useSSE()
const [kickCandidate, setKickCandidate] = useState<Player | null>(null)
const [promoteCandidate, setPromoteCandidate] = useState<Player | null>(null)
const { data: session } = useSession()
const currentUserSteamId = session?.user?.steamId || '' const currentUserSteamId = session?.user?.steamId || ''
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 [showRenameModal, setShowRenameModal] = useState(false)
const [activePlayers, setActivePlayers] = useState<Player[]>([])
const [inactivePlayers, setInactivePlayers] = useState<Player[]>([])
const [kickCandidate, setKickCandidate] = useState<Player | null>(null)
const [promoteCandidate, setPromoteCandidate] = useState<Player | null>(null)
const [showDeleteModal, setShowDeleteModal] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false)
const [isEditingName, setIsEditingName] = useState(false) const [isEditingName, setIsEditingName] = useState(false)
const [editedName, setEditedName] = useState(team?.name || '') const [editedName, setEditedName] = useState(team?.name || '')
const [isEditingLogo, setIsEditingLogo] = useState(false)
const [logoPreview, setLogoPreview] = useState<string | null>(null)
const [logoFile, setLogoFile] = useState<File | null>(null)
const [teamState, setTeamState] = useState<Team | null>(team) const [teamState, setTeamState] = useState<Team | null>(team)
const [saveSuccess, setSaveSuccess] = useState(false) const [saveSuccess, setSaveSuccess] = useState(false)
const [invitedPlayers, setInvitedPlayers] = useState<InvitedPlayer[]>([]) const [invitedPlayers, setInvitedPlayers] = useState<InvitedPlayer[]>([])
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(() => { useEffect(() => {
if (!team || !team.id) return
setTeamState(team) setTeamState(team)
}, [team]) setEditedName(team.name || '')
setActivePlayers(team.activePlayers)
setInactivePlayers(team.inactivePlayers)
useEffect(() => { const uniqueInvites = Array.from(
if (team?.invitedPlayers?.length) { new Map((team.invitedPlayers ?? []).map(p => [p.steamId, p])).values()
const unique = Array.from(
new Map(team.invitedPlayers.map(p => [p.steamId, p])).values()
) )
setInvitedPlayers(unique.sort((a, b) => a.name.localeCompare(b.name))) setInvitedPlayers(uniqueInvites.sort((a, b) => a.name.localeCompare(b.name)))
} else { }, [team?.id])
setInvitedPlayers([])
}
}, [team])
const handleDragStart = (event: any) => { const handleDragStart = (event: any) => {
const id = event.active.id const id = event.active.id
@ -163,7 +154,7 @@ export default function TeamMemberView({
newActive.sort((a, b) => a.name.localeCompare(b.name)) newActive.sort((a, b) => a.name.localeCompare(b.name))
newInactive.sort((a, b) => a.name.localeCompare(b.name)) newInactive.sort((a, b) => a.name.localeCompare(b.name))
setactivePlayers(newActive) setActivePlayers(newActive)
setInactivePlayers(newInactive) setInactivePlayers(newInactive)
await updateTeamMembers(team!.id, newActive, newInactive) await updateTeamMembers(team!.id, newActive, newInactive)
setSaveSuccess(true) setSaveSuccess(true)
@ -176,7 +167,7 @@ export default function TeamMemberView({
if (!updated) return if (!updated) return
setTeamState(updated) setTeamState(updated)
setactivePlayers(updated.activePlayers) setActivePlayers(updated.activePlayers)
setInactivePlayers(updated.inactivePlayers) setInactivePlayers(updated.inactivePlayers)
setInvitedPlayers(updated.invitedPlayers) setInvitedPlayers(updated.invitedPlayers)
} }
@ -187,7 +178,7 @@ export default function TeamMemberView({
const newActive = activePlayers.filter(p => p.steamId !== kickCandidate.steamId) const newActive = activePlayers.filter(p => p.steamId !== kickCandidate.steamId)
const newInactive = inactivePlayers.filter(p => p.steamId !== kickCandidate.steamId) const newInactive = inactivePlayers.filter(p => p.steamId !== kickCandidate.steamId)
setactivePlayers(newActive) setActivePlayers(newActive)
setInactivePlayers(newInactive) setInactivePlayers(newInactive)
await fetch('/api/team/kick', { await fetch('/api/team/kick', {

View File

@ -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<Team>(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
}

View File

@ -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<any>
) {
const [team, setTeam] = useState<Team | null>(null)
const [activePlayers, setactivePlayers] = useState<Player[]>([])
const [inactivePlayers, setInactivePlayers] = useState<Player[]>([])
const [showLeaveModal, setShowLeaveModal] = useState(false)
const [showInviteModal, setShowInviteModal] = useState(false)
const [activeDragItem, setActiveDragItem] = useState<Player | null>(null)
const [isDragging, setIsDragging] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [pendingInvitation, setPendingInvitation] = useState<Invitation | null>(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<boolean> => {
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,
}
}

View File

@ -1,21 +0,0 @@
'use client'
import { useEffect } from 'react'
export function useWebSocketListener<T = any>(
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])
}

View File

@ -8,7 +8,7 @@ import ThemeProvider from "@/theme/theme-provider";
import Script from "next/script"; import Script from "next/script";
import NotificationCenter from './components/NotificationCenter' import NotificationCenter from './components/NotificationCenter'
import Navbar from "./components/Navbar"; import Navbar from "./components/Navbar";
import SSEListener from "./components/SSEListener"; import SSEHandler from "./lib/SSEHandler";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -40,7 +40,7 @@ export default function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
<Providers> <Providers>
<SSEListener /> <SSEHandler />
{/* Sidebar und Content direkt nebeneinander */} {/* Sidebar und Content direkt nebeneinander */}
<Sidebar> <Sidebar>
{children} {children}

View File

@ -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
}

View File

@ -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 // 🔄 Team laden
export async function reloadTeam(teamId: string): Promise<Team | null> { export async function reloadTeam(teamId: string): Promise<Team | null> {
try { try {
console.log("reloadTeam");
const res = await fetch(`/api/team/${encodeURIComponent(teamId)}`) const res = await fetch(`/api/team/${encodeURIComponent(teamId)}`)
if (!res.ok) throw new Error('Fehler beim Abrufen des Teams') if (!res.ok) throw new Error('Fehler beim Abrufen des Teams')
@ -12,7 +13,7 @@ export async function reloadTeam(teamId: string): Promise<Team | null> {
const team = data.team ?? data const team = data.team ?? data
if (!team) return null if (!team) return null
const sortByName = (players: Player[]) => const sortByName = <T extends Player>(players: T[]): T[] =>
players.sort((a, b) => a.name.localeCompare(b.name)) players.sort((a, b) => a.name.localeCompare(b.name))
return { return {

View File

@ -4,6 +4,7 @@ const host = 'localhost'
export async function sendServerSSEMessage(message: any) { export async function sendServerSSEMessage(message: any) {
try { try {
console.log("Sending message: ", message);
await fetch(`http://${host}:3001/send`, { await fetch(`http://${host}:3001/send`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },

15
src/app/lib/stores.ts Normal file
View File

@ -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<TeamState>((set) => ({
team: null,
setTeam: (team) => set({ team }),
}))

View File

@ -1,51 +1,56 @@
// useSSEStore.ts
import { create } from 'zustand' import { create } from 'zustand'
type SSEEvent = {
type: string
payload: any
}
type SSEState = { type SSEState = {
source: EventSource | null source: EventSource | null
isConnected: boolean isConnected: boolean
connect: (steamId: string) => EventSource | undefined lastEvent: SSEEvent | null
connect: (steamId: string) => void
disconnect: () => void disconnect: () => void
} }
export const useSSE = create<SSEState>((set, get) => { export const useSSEStore = create<SSEState>((set, get) => {
let reconnectTimeout: NodeJS.Timeout | null = null let reconnectTimeout: NodeJS.Timeout | null = null
const connect = (steamId: string): EventSource | undefined => { const connect = (steamId: string) => {
const current = get().source if (get().source) return
if (current) return current
const source = new EventSource(`http://localhost:3001/events?steamId=${steamId}`) const source = new EventSource(`http://localhost:3001/events?steamId=${steamId}`)
source.onopen = () => { const eventTypes = [
console.log('[SSE] Verbunden!') 'team-updated',
set({ source, isConnected: true }) '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) => { for (const type of eventTypes) {
console.log('[SSE] Nachricht:', event.data) source.addEventListener(type, (event) => {
try {
const data = JSON.parse(event.data)
// Zentrale Weiterleitung aller Events, die ein "type" haben
if (data?.type) {
window.dispatchEvent(new CustomEvent(`sse-${data.type}`, { detail: data }))
}
} catch (err) {
console.error('[SSE] Ungültige Nachricht:', event.data)
}
}
source.addEventListener('notification', (event) => {
try { try {
const data = JSON.parse((event as MessageEvent).data) const data = JSON.parse((event as MessageEvent).data)
window.dispatchEvent(new CustomEvent(`sse-${data.type}`, { detail: data })) console.log(`[SSE] ${type}:`, data)
set({ lastEvent: { type, payload: data } })
} catch (err) { } catch (err) {
console.error('[SSE] Ungültige Nachricht:', event) console.error(`[SSE] Fehler beim Parsen von ${type}:`, err)
} }
}) })
}
source.onerror = (err) => { source.onerror = (err) => {
console.warn('[SSE] Verbindung verloren, versuche Reconnect...') console.warn('[SSE] Fehler, versuche Reconnect...')
source.close() source.close()
set({ source: null, isConnected: false }) set({ source: null, isConnected: false })
@ -57,25 +62,24 @@ export const useSSE = create<SSEState>((set, get) => {
} }
} }
source.onopen = () => {
console.log('[SSE] Verbunden!')
set({ source, isConnected: true })
}
set({ source }) set({ source })
return source // ✅ wichtig
} }
const disconnect = () => { const disconnect = () => {
const source = get().source get().source?.close()
if (source) { if (reconnectTimeout) clearTimeout(reconnectTimeout)
source.close()
}
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
reconnectTimeout = null
}
set({ source: null, isConnected: false }) set({ source: null, isConnected: false })
} }
return { return {
source: null, source: null,
isConnected: false, isConnected: false,
lastEvent: null,
connect, connect,
disconnect, disconnect,
} }