changes live-actions
This commit is contained in:
parent
404e577983
commit
903d898a0a
@ -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 { useTeamStore } from '@/app/lib/stores'
|
||||||
//import { useTeamManager } from '@/app/hooks/useTeamManager'
|
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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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(uniqueInvites.sort((a, b) => a.name.localeCompare(b.name)))
|
||||||
)
|
}, [team?.id])
|
||||||
setInvitedPlayers(unique.sort((a, b) => a.name.localeCompare(b.name)))
|
|
||||||
} else {
|
|
||||||
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', {
|
||||||
@ -265,410 +256,410 @@ export default function TeamMemberView({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`p-4 mt-6 bg-white border border-gray-200 shadow-2xs rounded-xl dark:bg-neutral-800 dark:border-neutral-700 ${isDragging ? 'cursor-grabbing' : ''}`}>
|
<div className={`p-4 mt-6 bg-white border border-gray-200 shadow-2xs rounded-xl dark:bg-neutral-800 dark:border-neutral-700 ${isDragging ? 'cursor-grabbing' : ''}`}>
|
||||||
<div className="flex justify-between items-center mb-6 flex-wrap gap-2">
|
<div className="flex justify-between items-center mb-6 flex-wrap gap-2">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Teamlogo mit Fallback */}
|
{/* Teamlogo mit Fallback */}
|
||||||
<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 && document.getElementById('logoUpload')?.click()}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={teamState.logo ? `/assets/img/logos/${teamState.logo}` : `/assets/img/logos/cs2.webp`}
|
src={teamState.logo ? `/assets/img/logos/${teamState.logo}` : `/assets/img/logos/cs2.webp`}
|
||||||
alt="Teamlogo"
|
alt="Teamlogo"
|
||||||
fill
|
fill
|
||||||
sizes="64px"
|
sizes="64px"
|
||||||
quality={75}
|
quality={75}
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
priority={false}
|
priority={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Overlay beim Hover */}
|
{/* Overlay beim Hover */}
|
||||||
{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">
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hidden file input */}
|
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<input
|
<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">
|
||||||
type="file"
|
<svg
|
||||||
accept="image/*"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
id="logoUpload"
|
className="w-5 h-5 mb-1"
|
||||||
className="hidden"
|
viewBox="0 0 576 512"
|
||||||
onChange={async (e) => {
|
fill="currentColor"
|
||||||
const file = e.target.files?.[0]
|
>
|
||||||
if (!file) return
|
<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>
|
||||||
const formData = new FormData()
|
</div>
|
||||||
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.')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Teamname + Bearbeiten */}
|
{/* Hidden file input */}
|
||||||
<div className="flex items-center gap-2">
|
{canManage && (
|
||||||
{isEditingName ? (
|
<input
|
||||||
<>
|
type="file"
|
||||||
<input
|
accept="image/*"
|
||||||
type="text"
|
id="logoUpload"
|
||||||
value={editedName}
|
className="hidden"
|
||||||
onChange={(e) => setEditedName(e.target.value)}
|
onChange={async (e) => {
|
||||||
className="py-1.5 px-3 border rounded-lg text-sm dark:bg-neutral-800 dark:border-neutral-700 dark:text-white"
|
const file = e.target.files?.[0]
|
||||||
/>
|
if (!file) return
|
||||||
|
|
||||||
{/* ✔ Übernehmen */}
|
const formData = new FormData()
|
||||||
<Button
|
formData.append('logo', file)
|
||||||
title="Übernehmen"
|
formData.append('teamId', teamState.id)
|
||||||
color="green"
|
|
||||||
size="sm"
|
|
||||||
variant="soft"
|
|
||||||
onClick={async () => {
|
|
||||||
await renameTeam(teamState.id, editedName)
|
|
||||||
setTeamState((prev) => prev ? { ...prev, teamname: editedName } : prev)
|
|
||||||
setIsEditingName(false)
|
|
||||||
await handleReload()
|
|
||||||
}}
|
|
||||||
className="h-[34px] px-3 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="w-4 h-4"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* ✖ Abbrechen */}
|
const res = await fetch('/api/team/upload-logo', {
|
||||||
<Button
|
method: 'POST',
|
||||||
title="Abbrechen"
|
body: formData,
|
||||||
color="red"
|
})
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
if (res.ok) {
|
||||||
onClick={() => {
|
await handleReload()
|
||||||
setIsEditingName(false)
|
} else {
|
||||||
setEditedName(teamState.name ?? '')
|
alert('Fehler beim Hochladen des Logos.')
|
||||||
}}
|
}
|
||||||
className="h-[34px] px-3 flex items-center justify-center"
|
}}
|
||||||
>
|
/>
|
||||||
<svg
|
)}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="w-4 h-4"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 dark:text-white">
|
|
||||||
{teamState.name ?? 'Team'}
|
|
||||||
</h2>
|
|
||||||
<TeamPremierRankBadge players={activePlayers} />
|
|
||||||
</div>
|
|
||||||
{canManage && (
|
|
||||||
<Button
|
|
||||||
title="Bearbeiten"
|
|
||||||
color="blue"
|
|
||||||
size="sm"
|
|
||||||
variant="soft"
|
|
||||||
onClick={() => {
|
|
||||||
setIsEditingName(true)
|
|
||||||
setEditedName(teamState.name || '')
|
|
||||||
}}
|
|
||||||
className="h-[34px] px-3 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="w-4 h-4"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M17.414 2.586a2 2 0 010 2.828l-9.793 9.793a1 1 0 01-.293.207l-4 2a1 1 0 01-1.32-1.32l2-4a1 1 0 01.207-.293l9.793-9.793a2 2 0 012.828 0zM15.586 4L6 13.586l-1.293 2.586L7.414 14 17 4.414 15.586 4z" />
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Aktionen */}
|
{/* Teamname + Bearbeiten */}
|
||||||
<div className="flex gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{canManage && (
|
{isEditingName ? (
|
||||||
<button
|
<>
|
||||||
type="button"
|
<input
|
||||||
onClick={() => setShowDeleteModal(true)}
|
type="text"
|
||||||
className="text-sm px-3 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
value={editedName}
|
||||||
>
|
onChange={(e) => setEditedName(e.target.value)}
|
||||||
Team löschen
|
className="py-1.5 px-3 border rounded-lg text-sm dark:bg-neutral-800 dark:border-neutral-700 dark:text-white"
|
||||||
</button>
|
/>
|
||||||
|
|
||||||
|
{/* ✔ Übernehmen */}
|
||||||
|
<Button
|
||||||
|
title="Übernehmen"
|
||||||
|
color="green"
|
||||||
|
size="sm"
|
||||||
|
variant="soft"
|
||||||
|
onClick={async () => {
|
||||||
|
await renameTeam(teamState.id, editedName)
|
||||||
|
setTeamState((prev) => prev ? { ...prev, teamname: editedName } : prev)
|
||||||
|
setIsEditingName(false)
|
||||||
|
await handleReload()
|
||||||
|
}}
|
||||||
|
className="h-[34px] px-3 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* ✖ Abbrechen */}
|
||||||
|
<Button
|
||||||
|
title="Abbrechen"
|
||||||
|
color="red"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditingName(false)
|
||||||
|
setEditedName(teamState.name ?? '')
|
||||||
|
}}
|
||||||
|
className="h-[34px] px-3 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||||||
|
{teamState.name ?? 'Team'}
|
||||||
|
</h2>
|
||||||
|
<TeamPremierRankBadge players={activePlayers} />
|
||||||
|
</div>
|
||||||
|
{canManage && (
|
||||||
|
<Button
|
||||||
|
title="Bearbeiten"
|
||||||
|
color="blue"
|
||||||
|
size="sm"
|
||||||
|
variant="soft"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditingName(true)
|
||||||
|
setEditedName(teamState.name || '')
|
||||||
|
}}
|
||||||
|
className="h-[34px] px-3 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-4 h-4"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M17.414 2.586a2 2 0 010 2.828l-9.793 9.793a1 1 0 01-.293.207l-4 2a1 1 0 01-1.32-1.32l2-4a1 1 0 01.207-.293l9.793-9.793a2 2 0 012.828 0zM15.586 4L6 13.586l-1.293 2.586L7.414 14 17 4.414 15.586 4z" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
if (isLeader) {
|
|
||||||
setShowLeaveModal(true)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await leaveTeam(currentUserSteamId)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fehler beim Verlassen:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="text-sm px-3 py-1.5 bg-gray-200 text-black rounded-lg hover:bg-gray-300 dark:bg-neutral-700 dark:text-white dark:hover:bg-neutral-600"
|
|
||||||
>
|
|
||||||
Team verlassen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DndContext collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
{/* Aktionen */}
|
||||||
<div className="space-y-8">
|
<div className="flex gap-2">
|
||||||
<DroppableZone id="active" label={`Aktive Spieler (${activePlayers.length} / 5)`} activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
{canManage && (
|
||||||
<SortableContext items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
<button
|
||||||
{renderMemberList(activePlayers)}
|
type="button"
|
||||||
</SortableContext>
|
onClick={() => setShowDeleteModal(true)}
|
||||||
</DroppableZone>
|
className="text-sm px-3 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Team löschen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
if (isLeader) {
|
||||||
|
setShowLeaveModal(true)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await leaveTeam(currentUserSteamId)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Verlassen:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-sm px-3 py-1.5 bg-gray-200 text-black rounded-lg hover:bg-gray-300 dark:bg-neutral-700 dark:text-white dark:hover:bg-neutral-600"
|
||||||
|
>
|
||||||
|
Team verlassen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DroppableZone id="inactive" label="Inaktive Spieler" activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
<DndContext collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||||
<SortableContext items={inactivePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
<div className="space-y-8">
|
||||||
{renderMemberList(inactivePlayers)}
|
<DroppableZone id="active" label={`Aktive Spieler (${activePlayers.length} / 5)`} activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
||||||
{canManage && (
|
<SortableContext items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||||
<motion.div key="mini-card-dummy" initial={{ opacity: 0 }} animate={{ opacity: 1}} exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
|
{renderMemberList(activePlayers)}
|
||||||
<MiniCardDummy
|
</SortableContext>
|
||||||
title={canAddDirect ? 'Hinzufügen' : 'Einladen'}
|
</DroppableZone>
|
||||||
onClick={() => {
|
|
||||||
setShowInviteModal(false)
|
|
||||||
setTimeout(() => setShowInviteModal(true), 0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center w-16 h-16 bg-white rounded-full">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-8 h-8 text-black" fill="currentColor" viewBox="0 0 640 512">
|
|
||||||
<path d="M96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM0 482.3C0 383.8 79.8 304 178.3 304h91.4C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3zM504 312v-64h-64c-13.3 0-24-10.7-24-24s10.7-24 24-24h64v-64c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24h-64v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</MiniCardDummy>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</SortableContext>
|
|
||||||
</DroppableZone>
|
|
||||||
|
|
||||||
{invitedPlayers.length > 0 && (
|
<DroppableZone id="inactive" label="Inaktive Spieler" activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
||||||
<div className="space-y-2">
|
<SortableContext items={inactivePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||||
<div className="flex items-center justify-between">
|
{renderMemberList(inactivePlayers)}
|
||||||
<h3 className="text-md font-semibold text-gray-700 dark:text-gray-300">Eingeladene Spieler</h3>
|
{canManage && (
|
||||||
</div>
|
<motion.div key="mini-card-dummy" initial={{ opacity: 0 }} animate={{ opacity: 1}} exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
|
||||||
<div className="w-full rounded-lg p-4 transition-colors min-h-[200px] border border-gray-300 dark:border-neutral-700">
|
<MiniCardDummy
|
||||||
<div className="grid gap-4 justify-start grid-cols-[repeat(auto-fill,minmax(160px,1fr))]">
|
title={canAddDirect ? 'Hinzufügen' : 'Einladen'}
|
||||||
<AnimatePresence>
|
onClick={() => {
|
||||||
{invitedPlayers.map((player: InvitedPlayer) => (
|
setShowInviteModal(false)
|
||||||
<motion.div
|
setTimeout(() => setShowInviteModal(true), 0)
|
||||||
key={player.steamId}
|
}}
|
||||||
initial={{ opacity: 0, y: 10 }}
|
>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<div className="flex items-center justify-center w-16 h-16 bg-white rounded-full">
|
||||||
exit={{ opacity: 0, y: -10 }}
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-8 h-8 text-black" fill="currentColor" viewBox="0 0 640 512">
|
||||||
transition={{ duration: 0.2 }}
|
<path d="M96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM0 482.3C0 383.8 79.8 304 178.3 304h91.4C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3zM504 312v-64h-64c-13.3 0-24-10.7-24-24s10.7-24 24-24h64v-64c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24h-64v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z" />
|
||||||
>
|
</svg>
|
||||||
<MiniCard
|
</div>
|
||||||
steamId={player.steamId}
|
</MiniCardDummy>
|
||||||
title={player.name}
|
</motion.div>
|
||||||
avatar={player.avatar}
|
)}
|
||||||
location={player.location}
|
</SortableContext>
|
||||||
selected={false}
|
</DroppableZone>
|
||||||
onSelect={() => {}}
|
|
||||||
draggable={false}
|
{invitedPlayers.length > 0 && (
|
||||||
currentUserSteamId={currentUserSteamId}
|
<div className="space-y-2">
|
||||||
teamLeaderSteamId={teamState.leader}
|
<div className="flex items-center justify-between">
|
||||||
isSelectable={false}
|
<h3 className="text-md font-semibold text-gray-700 dark:text-gray-300">Eingeladene Spieler</h3>
|
||||||
isInvite={true}
|
</div>
|
||||||
rank={player.premierRank}
|
<div className="w-full rounded-lg p-4 transition-colors min-h-[200px] border border-gray-300 dark:border-neutral-700">
|
||||||
onKick={revokeInvitation}
|
<div className="grid gap-4 justify-start grid-cols-[repeat(auto-fill,minmax(160px,1fr))]">
|
||||||
invitationId={player.invitationId}
|
<AnimatePresence>
|
||||||
/>
|
{invitedPlayers.map((player: InvitedPlayer) => (
|
||||||
</motion.div>
|
<motion.div
|
||||||
))}
|
key={player.steamId}
|
||||||
</AnimatePresence>
|
initial={{ opacity: 0, y: 10 }}
|
||||||
</div>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<MiniCard
|
||||||
|
steamId={player.steamId}
|
||||||
|
title={player.name}
|
||||||
|
avatar={player.avatar}
|
||||||
|
location={player.location}
|
||||||
|
selected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
draggable={false}
|
||||||
|
currentUserSteamId={currentUserSteamId}
|
||||||
|
teamLeaderSteamId={teamState.leader}
|
||||||
|
isSelectable={false}
|
||||||
|
isInvite={true}
|
||||||
|
rank={player.premierRank}
|
||||||
|
onKick={revokeInvitation}
|
||||||
|
invitationId={player.invitationId}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DragOverlay>
|
||||||
|
{activeDragItem && (
|
||||||
|
<SortableMiniCard
|
||||||
|
player={activeDragItem}
|
||||||
|
currentUserSteamId={currentUserSteamId}
|
||||||
|
teamLeaderSteamId={teamState.leader}
|
||||||
|
isAdmin={!!session?.user?.isAdmin}
|
||||||
|
hideOverlay
|
||||||
|
matchParentBg
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
|
|
||||||
|
{/* Modal(s) */}
|
||||||
|
{canInvite && (
|
||||||
|
<InvitePlayersModal
|
||||||
|
show={showInviteModal}
|
||||||
|
onClose={() => setShowInviteModal(false)}
|
||||||
|
onSuccess={() => {}}
|
||||||
|
team={teamState}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canAddDirect && (
|
||||||
|
<InvitePlayersModal
|
||||||
|
show={showInviteModal}
|
||||||
|
onClose={() => setShowInviteModal(false)}
|
||||||
|
onSuccess={() => {}}
|
||||||
|
team={teamState}
|
||||||
|
directAdd
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Leader-spezifische Modale (z. B. Team verlassen) */}
|
||||||
|
{isLeader && (
|
||||||
|
<LeaveTeamModal
|
||||||
|
show={showLeaveModal}
|
||||||
|
onClose={() => setShowLeaveModal(false)}
|
||||||
|
onSuccess={() => setShowLeaveModal(false)}
|
||||||
|
team={teamState}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{canManage && promoteCandidate && (
|
||||||
|
<Modal
|
||||||
|
id={`modal-promote-player-${promoteCandidate.steamId}`}
|
||||||
|
title="Leader übertragen"
|
||||||
|
show={true}
|
||||||
|
onClose={() => setPromoteCandidate(null)}
|
||||||
|
onSave={async () => {
|
||||||
|
await promoteToLeader(promoteCandidate.steamId)
|
||||||
|
setPromoteCandidate(null)
|
||||||
|
}}
|
||||||
|
closeButtonTitle="Übertragen"
|
||||||
|
closeButtonColor="blue"
|
||||||
|
>
|
||||||
|
{/* ► PlayerCard des Kandidaten */}
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<MiniCard
|
||||||
|
steamId={promoteCandidate.steamId}
|
||||||
|
title={promoteCandidate.name}
|
||||||
|
avatar={promoteCandidate.avatar}
|
||||||
|
location={promoteCandidate.location}
|
||||||
|
selected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
draggable={false}
|
||||||
|
currentUserSteamId={currentUserSteamId}
|
||||||
|
teamLeaderSteamId={teamState.leader}
|
||||||
|
hideActions
|
||||||
|
isSelectable={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DragOverlay>
|
<p className="text-sm text-gray-700 dark:text-neutral-300">
|
||||||
{activeDragItem && (
|
Möchtest du <strong>{promoteCandidate.name}</strong> wirklich zum Team-Leader machen?
|
||||||
<SortableMiniCard
|
</p>
|
||||||
player={activeDragItem}
|
</Modal>
|
||||||
currentUserSteamId={currentUserSteamId}
|
)}
|
||||||
teamLeaderSteamId={teamState.leader}
|
|
||||||
isAdmin={!!session?.user?.isAdmin}
|
|
||||||
hideOverlay
|
|
||||||
matchParentBg
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
|
|
||||||
{/* Modal(s) */}
|
{canManage && kickCandidate && (
|
||||||
{canInvite && (
|
<Modal
|
||||||
<InvitePlayersModal
|
id={`modal-kick-player-${kickCandidate.steamId}`}
|
||||||
show={showInviteModal}
|
title="Mitglied entfernen"
|
||||||
onClose={() => setShowInviteModal(false)}
|
show={true}
|
||||||
onSuccess={() => {}}
|
onClose={() => setKickCandidate(null)}
|
||||||
team={teamState}
|
onSave={confirmKick}
|
||||||
/>
|
closeButtonTitle="Entfernen"
|
||||||
)}
|
closeButtonColor="red"
|
||||||
|
>
|
||||||
|
{/* ► PlayerCard des Kandidaten */}
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<MiniCard
|
||||||
|
steamId={kickCandidate.steamId}
|
||||||
|
title={kickCandidate.name}
|
||||||
|
avatar={kickCandidate.avatar}
|
||||||
|
location={kickCandidate.location}
|
||||||
|
selected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
draggable={false}
|
||||||
|
currentUserSteamId={currentUserSteamId}
|
||||||
|
teamLeaderSteamId={teamState.leader}
|
||||||
|
hideActions
|
||||||
|
isSelectable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{canAddDirect && (
|
<p className="text-sm text-gray-700 dark:text-neutral-300">
|
||||||
<InvitePlayersModal
|
Möchtest du <strong>{kickCandidate.name}</strong> wirklich aus dem Team entfernen?
|
||||||
show={showInviteModal}
|
</p>
|
||||||
onClose={() => setShowInviteModal(false)}
|
</Modal>
|
||||||
onSuccess={() => {}}
|
)}
|
||||||
team={teamState}
|
|
||||||
directAdd
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Leader-spezifische Modale (z. B. Team verlassen) */}
|
{canManage && (
|
||||||
{isLeader && (
|
<Modal
|
||||||
<LeaveTeamModal
|
id="modal-delete-team"
|
||||||
show={showLeaveModal}
|
title="Team löschen"
|
||||||
onClose={() => setShowLeaveModal(false)}
|
show={showDeleteModal}
|
||||||
onSuccess={() => setShowLeaveModal(false)}
|
onClose={() => setShowDeleteModal(false)}
|
||||||
team={teamState}
|
onSave={async () => {
|
||||||
/>
|
await fetch('/api/team/delete', {
|
||||||
)}
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ teamId: teamState.id }),
|
||||||
{canManage && promoteCandidate && (
|
})
|
||||||
<Modal
|
setShowDeleteModal(false)
|
||||||
id={`modal-promote-player-${promoteCandidate.steamId}`}
|
window.location.href = '/'
|
||||||
title="Leader übertragen"
|
}}
|
||||||
show={true}
|
closeButtonTitle="Löschen"
|
||||||
onClose={() => setPromoteCandidate(null)}
|
closeButtonColor="red"
|
||||||
onSave={async () => {
|
>
|
||||||
await promoteToLeader(promoteCandidate.steamId)
|
<p className="text-sm text-gray-700 dark:text-neutral-300">
|
||||||
setPromoteCandidate(null)
|
Bist du sicher, dass du dieses Team löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.
|
||||||
}}
|
</p>
|
||||||
closeButtonTitle="Übertragen"
|
</Modal>
|
||||||
closeButtonColor="blue"
|
)}
|
||||||
>
|
</div>
|
||||||
{/* ► PlayerCard des Kandidaten */}
|
|
||||||
<div className="flex justify-center mb-4">
|
|
||||||
<MiniCard
|
|
||||||
steamId={promoteCandidate.steamId}
|
|
||||||
title={promoteCandidate.name}
|
|
||||||
avatar={promoteCandidate.avatar}
|
|
||||||
location={promoteCandidate.location}
|
|
||||||
selected={false}
|
|
||||||
onSelect={() => {}}
|
|
||||||
draggable={false}
|
|
||||||
currentUserSteamId={currentUserSteamId}
|
|
||||||
teamLeaderSteamId={teamState.leader}
|
|
||||||
hideActions
|
|
||||||
isSelectable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-700 dark:text-neutral-300">
|
|
||||||
Möchtest du <strong>{promoteCandidate.name}</strong> wirklich zum Team-Leader machen?
|
|
||||||
</p>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canManage && kickCandidate && (
|
|
||||||
<Modal
|
|
||||||
id={`modal-kick-player-${kickCandidate.steamId}`}
|
|
||||||
title="Mitglied entfernen"
|
|
||||||
show={true}
|
|
||||||
onClose={() => setKickCandidate(null)}
|
|
||||||
onSave={confirmKick}
|
|
||||||
closeButtonTitle="Entfernen"
|
|
||||||
closeButtonColor="red"
|
|
||||||
>
|
|
||||||
{/* ► PlayerCard des Kandidaten */}
|
|
||||||
<div className="flex justify-center mb-4">
|
|
||||||
<MiniCard
|
|
||||||
steamId={kickCandidate.steamId}
|
|
||||||
title={kickCandidate.name}
|
|
||||||
avatar={kickCandidate.avatar}
|
|
||||||
location={kickCandidate.location}
|
|
||||||
selected={false}
|
|
||||||
onSelect={() => {}}
|
|
||||||
draggable={false}
|
|
||||||
currentUserSteamId={currentUserSteamId}
|
|
||||||
teamLeaderSteamId={teamState.leader}
|
|
||||||
hideActions
|
|
||||||
isSelectable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-700 dark:text-neutral-300">
|
|
||||||
Möchtest du <strong>{kickCandidate.name}</strong> wirklich aus dem Team entfernen?
|
|
||||||
</p>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canManage && (
|
|
||||||
<Modal
|
|
||||||
id="modal-delete-team"
|
|
||||||
title="Team löschen"
|
|
||||||
show={showDeleteModal}
|
|
||||||
onClose={() => 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"
|
|
||||||
>
|
|
||||||
<p className="text-sm text-gray-700 dark:text-neutral-300">
|
|
||||||
Bist du sicher, dass du dieses Team löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.
|
|
||||||
</p>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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])
|
|
||||||
}
|
|
||||||
@ -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}
|
||||||
|
|||||||
56
src/app/lib/SSEHandler.tsx
Normal file
56
src/app/lib/SSEHandler.tsx
Normal 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
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
@ -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
15
src/app/lib/stores.ts
Normal 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 }),
|
||||||
|
}))
|
||||||
@ -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 {
|
try {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse((event as MessageEvent).data)
|
||||||
|
console.log(`[SSE] ${type}:`, data)
|
||||||
|
|
||||||
// Zentrale Weiterleitung aller Events, die ein "type" haben
|
set({ lastEvent: { type, payload: data } })
|
||||||
if (data?.type) {
|
|
||||||
window.dispatchEvent(new CustomEvent(`sse-${data.type}`, { detail: 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) => {
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user