update
This commit is contained in:
parent
e8e9632512
commit
63c6c9f87a
@ -8,16 +8,23 @@ export async function GET(
|
||||
{ params }: { params: { teamId: string } },
|
||||
) {
|
||||
try {
|
||||
/* ─── 1) Team holen ─────────────────────────────── */
|
||||
/* ─── 1) Team + Invites holen ───────────────────────────── */
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: params.teamId },
|
||||
include: {
|
||||
invites: {
|
||||
include: {
|
||||
user: true, // ⬅ notwendig für eingeladenen Spieler
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!team) {
|
||||
return NextResponse.json({ error: 'Team nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
/* ─── 2) Alle Steam-IDs sammeln und Users laden ─── */
|
||||
/* ─── 2) Aktive + Inaktive Spieler holen ─────────────────── */
|
||||
const allIds = Array.from(
|
||||
new Set([...team.activePlayers, ...team.inactivePlayers]),
|
||||
)
|
||||
@ -33,7 +40,6 @@ export async function GET(
|
||||
},
|
||||
})
|
||||
|
||||
/* Map steamId → Player */
|
||||
const byId: Record<string, Player> = Object.fromEntries(
|
||||
users.map(u => [
|
||||
u.steamId,
|
||||
@ -47,7 +53,6 @@ export async function GET(
|
||||
]),
|
||||
)
|
||||
|
||||
/* ─── 3) Arrays umwandeln + sortieren ───────────── */
|
||||
const activePlayers = team.activePlayers
|
||||
.map(id => byId[id])
|
||||
.filter(Boolean)
|
||||
@ -58,7 +63,19 @@ export async function GET(
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
/* ─── 4) Antwort zusammenbauen ───────────────────── */
|
||||
/* ─── 3) Eingeladene Spieler extrahieren ─────────────────── */
|
||||
const invitedPlayers: Player[] = team.invites.map(invite => {
|
||||
const u = invite.user
|
||||
return {
|
||||
steamId : u.steamId,
|
||||
name : u.name ?? 'Unbekannt',
|
||||
avatar : u.avatar ?? '/assets/img/avatars/default.png',
|
||||
location : u.location ?? '',
|
||||
premierRank: u.premierRank ?? 0,
|
||||
}
|
||||
}).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
/* ─── 4) Antwort zurückgeben ─────────────────────────────── */
|
||||
const result = {
|
||||
id : team.id,
|
||||
name : team.name,
|
||||
@ -67,6 +84,7 @@ export async function GET(
|
||||
createdAt : team.createdAt,
|
||||
activePlayers,
|
||||
inactivePlayers,
|
||||
invitedPlayers,
|
||||
}
|
||||
|
||||
return NextResponse.json(result)
|
||||
|
||||
@ -3,6 +3,43 @@ import { prisma } from '@/app/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// 1. Alle offenen Einladungen (inkl. User-Infos)
|
||||
const pendingInvites = await prisma.teamInvite.findMany({
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
steamId : true,
|
||||
name : true,
|
||||
avatar : true,
|
||||
location : true,
|
||||
premierRank: true,
|
||||
team : true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 2. Steam-IDs aus Einladungen extrahieren
|
||||
const invitedSteamIds = new Set(
|
||||
pendingInvites
|
||||
.map(inv => inv.user?.steamId)
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
// 3. Alle Teams laden (nur SteamIDs)
|
||||
const teams = await prisma.team.findMany({
|
||||
select: {
|
||||
activePlayers: true,
|
||||
inactivePlayers: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 4. Steam-IDs aller Teammitglieder sammeln
|
||||
const teamMemberIds = new Set(
|
||||
teams.flatMap(team => [...team.activePlayers, ...team.inactivePlayers])
|
||||
)
|
||||
|
||||
// 5. Alle Benutzer laden, die kein Team haben
|
||||
const allUsers = await prisma.user.findMany({
|
||||
where: {
|
||||
team: null,
|
||||
@ -19,7 +56,13 @@ export async function GET() {
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ users: allUsers })
|
||||
// 6. Nur Benutzer behalten, die **nicht** eingeladen und **nicht** bereits im Team sind
|
||||
const availableUsers = allUsers.filter(user =>
|
||||
!invitedSteamIds.has(user.steamId) &&
|
||||
!teamMemberIds.has(user.steamId)
|
||||
)
|
||||
|
||||
return NextResponse.json({ users: availableUsers })
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der verfügbaren Benutzer:', error)
|
||||
return NextResponse.json({ message: 'Fehler beim Laden' }, { status: 500 })
|
||||
|
||||
@ -19,7 +19,7 @@ export async function POST(req: NextRequest) {
|
||||
/* Team holen */
|
||||
const team = await prisma.team.findUnique({
|
||||
where : { id: teamId },
|
||||
select: { name: true },
|
||||
select: { name: true, leader: true },
|
||||
})
|
||||
if (!team) {
|
||||
return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
|
||||
@ -62,6 +62,12 @@ export async function POST(req: NextRequest) {
|
||||
createdAt : notification.createdAt.toISOString(),
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: 'team-updated',
|
||||
teamId,
|
||||
targetUserIds: team.leader?.steamId,
|
||||
})
|
||||
|
||||
return invite.id
|
||||
}),
|
||||
)
|
||||
|
||||
@ -2,6 +2,7 @@ import { getServerSession } from 'next-auth'
|
||||
import { baseAuthOptions } from '@/app/lib/auth'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { InvitedPlayer } from '@/app/types/team'
|
||||
|
||||
export async function GET() {
|
||||
const session = await getServerSession(baseAuthOptions)
|
||||
@ -20,19 +21,25 @@ export async function GET() {
|
||||
],
|
||||
},
|
||||
include: {
|
||||
leader: true,
|
||||
matchesAsTeamA: {
|
||||
include: {
|
||||
teamA: true,
|
||||
teamB: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
matchesAsTeamB: {
|
||||
include: {
|
||||
teamA: true,
|
||||
teamB: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
invites: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!team) {
|
||||
@ -43,7 +50,13 @@ export async function GET() {
|
||||
|
||||
const playerData = await prisma.user.findMany({
|
||||
where: { steamId: { in: steamIds } },
|
||||
select: { steamId: true, name: true, avatar: true, location: true, premierRank: true },
|
||||
select: {
|
||||
steamId: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
location: true,
|
||||
premierRank: true,
|
||||
},
|
||||
})
|
||||
|
||||
const activePlayers = team.activePlayers
|
||||
@ -54,6 +67,25 @@ export async function GET() {
|
||||
.map((id) => playerData.find((m) => m.steamId === id))
|
||||
.filter(Boolean)
|
||||
|
||||
const invitedPlayers: InvitedPlayer[] = Array.from(
|
||||
new Map(
|
||||
team.invites.map(invite => {
|
||||
const u = invite.user
|
||||
return [
|
||||
u.steamId,
|
||||
{
|
||||
steamId: u.steamId,
|
||||
name: u.name ?? 'Unbekannt',
|
||||
avatar: u.avatar ?? '/assets/img/avatars/default.png',
|
||||
location: u.location ?? '',
|
||||
premierRank: u.premierRank ?? 0,
|
||||
invitationId: invite.id,
|
||||
}
|
||||
]
|
||||
})
|
||||
).values()
|
||||
).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
const matches = [...team.matchesAsTeamA, ...team.matchesAsTeamB]
|
||||
.filter(m => m.teamA && m.teamB)
|
||||
.sort((a, b) => new Date(a.matchDate).getTime() - new Date(b.matchDate).getTime())
|
||||
@ -63,9 +95,10 @@ export async function GET() {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
logo: team.logo,
|
||||
leader: team.leaderId,
|
||||
leader: team.leader?.steamId ?? null,
|
||||
activePlayers,
|
||||
inactivePlayers,
|
||||
invitedPlayers,
|
||||
matches,
|
||||
},
|
||||
})
|
||||
|
||||
@ -5,18 +5,49 @@ import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { teamId, activePlayers, inactivePlayers } = await req.json()
|
||||
const { teamId, activePlayers, inactivePlayers, invitedPlayers } = await req.json()
|
||||
|
||||
if (!teamId || !Array.isArray(activePlayers) || !Array.isArray(inactivePlayers)) {
|
||||
return NextResponse.json({ error: 'Ungültige Eingabedaten' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 🟢 Team-Update: aktive & inaktive Spieler
|
||||
await prisma.team.update({
|
||||
where: { id: teamId },
|
||||
data: { activePlayers, inactivePlayers },
|
||||
})
|
||||
|
||||
const allSteamIds = [...activePlayers, ...inactivePlayers]
|
||||
// 🔄 Einladungen synchronisieren
|
||||
if (Array.isArray(invitedPlayers)) {
|
||||
// Zuerst: Bestehende Einladungen für dieses Team laden
|
||||
const existingInvites = await prisma.teamInvite.findMany({
|
||||
where: { teamId },
|
||||
})
|
||||
|
||||
const existingSteamIds = existingInvites.map((invite) => invite.steamId)
|
||||
const toAdd = invitedPlayers.filter((id: string) => !existingSteamIds.includes(id))
|
||||
const toRemove = existingSteamIds.filter((id) => !invitedPlayers.includes(id))
|
||||
|
||||
// Neue Einladungen erstellen
|
||||
await prisma.teamInvite.createMany({
|
||||
data: toAdd.map((steamId: string) => ({
|
||||
teamId,
|
||||
steamId,
|
||||
type: 'invite',
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
|
||||
// Nicht mehr gelistete Einladungen löschen
|
||||
await prisma.teamInvite.deleteMany({
|
||||
where: {
|
||||
teamId,
|
||||
steamId: { in: toRemove },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const allSteamIds = [...activePlayers, ...inactivePlayers, ...(invitedPlayers || [])]
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: 'team-updated',
|
||||
|
||||
@ -133,6 +133,17 @@ export async function POST(
|
||||
return NextResponse.json({ message: 'Einladung abgelehnt' })
|
||||
}
|
||||
|
||||
if (action === 'revoke') {
|
||||
await prisma.teamInvite.delete({ where: { id: invitationId } })
|
||||
|
||||
await prisma.notification.updateMany({
|
||||
where: { steamId, actionData: invitationId },
|
||||
data: { read: true, actionType: null, actionData: null },
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Einladung gelöscht' })
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Ungültige Aktion' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Einladung:', error)
|
||||
|
||||
@ -10,7 +10,7 @@ type ButtonProps = {
|
||||
modalId?: string
|
||||
color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent'
|
||||
variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
size?: 'xs' |'sm' | 'md' | 'lg'
|
||||
className?: string
|
||||
dropDirection?: "up" | "down" | "auto"
|
||||
disabled?: boolean
|
||||
@ -47,6 +47,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
: {}
|
||||
|
||||
const sizeClasses: Record<string, string> = {
|
||||
xs: 'py-1 px-2',
|
||||
sm: 'py-2 px-3',
|
||||
md: 'py-3 px-4',
|
||||
lg: 'p-4 sm:p-5',
|
||||
|
||||
@ -59,12 +59,7 @@ export function DroppableZone({
|
||||
|
||||
{/* Hier sitzt der Droppable-Ref */}
|
||||
<div ref={setNodeRef} className={zoneClasses}>
|
||||
<div
|
||||
className="
|
||||
grid gap-4 justify-start
|
||||
[grid-template-columns:repeat(5,minmax(0,160px))]
|
||||
"
|
||||
>
|
||||
<div className="grid gap-4 justify-start grid-cols-[repeat(auto-fill,minmax(160px,1fr))]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -23,49 +23,48 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
|
||||
const [allUsers, setAllUsers] = useState<Player[]>([])
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [invitedIds, setInvitedIds] = useState<string[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
const [sentCount, setSentCount] = useState(0)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const usersPerPage = 9
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
fetchUsersNotInTeam()
|
||||
setIsSuccess(false) // Status zurücksetzen beim Öffnen
|
||||
setIsSuccess(false)
|
||||
setInvitedIds([])
|
||||
}
|
||||
}, [show])
|
||||
|
||||
const fetchUsersNotInTeam = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setIsLoading(true)
|
||||
const res = await fetch('/api/team/available-users')
|
||||
const data = await res.json()
|
||||
setAllUsers(data.users || [])
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Benutzer:', err)
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = (steamId: string) => {
|
||||
setSelectedIds((prev) =>
|
||||
prev.includes(steamId) ? prev.filter((id) => id !== steamId) : [...prev, steamId]
|
||||
prev.includes(steamId)
|
||||
? prev.filter((id) => id !== steamId)
|
||||
: [...prev, steamId]
|
||||
)
|
||||
}
|
||||
|
||||
// Entferne das setTimeout aus handleInvite komplett!
|
||||
|
||||
const handleInvite = async () => {
|
||||
if (selectedIds.length === 0 || !steamId) return
|
||||
|
||||
try {
|
||||
const url = directAdd ? '/api/team/add-players'
|
||||
: '/api/team/invite'
|
||||
const url = directAdd ? '/api/team/add-players' : '/api/team/invite'
|
||||
const body = directAdd
|
||||
? { teamId: team.id, steamIds: selectedIds }
|
||||
: { teamId: team.id, userIds: selectedIds, invitedBy: steamId }
|
||||
@ -76,15 +75,15 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json()
|
||||
console.error('Fehler beim Einladen:', error.message)
|
||||
} else {
|
||||
setSentCount(selectedIds.length) // 👈 speichere Anzahl
|
||||
setIsSuccess(true) // ✅ Erfolg markieren
|
||||
setSelectedIds([]) // Optional: Selektion leeren
|
||||
onSuccess() // ⚡ Nur Success-Callback, kein Schließen hier!
|
||||
setSentCount(selectedIds.length)
|
||||
setInvitedIds(selectedIds) // 👈 Einladungsliste speichern
|
||||
setIsSuccess(true)
|
||||
setSelectedIds([]) // ⛔ nicht zu früh löschen!
|
||||
onSuccess()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Einladen:', err)
|
||||
@ -99,7 +98,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
window.HSOverlay.close(modalEl)
|
||||
}
|
||||
onClose()
|
||||
}, 1500)
|
||||
}, 2000)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
@ -119,7 +118,11 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
return aSelected - bSelected
|
||||
})
|
||||
|
||||
const unselectedUsers = filteredUsers.filter((user) => !selectedIds.includes(user.steamId))
|
||||
const unselectedUsers = filteredUsers.filter((user) =>
|
||||
!selectedIds.includes(user.steamId) &&
|
||||
(!isSuccess || !invitedIds.includes(user.steamId))
|
||||
)
|
||||
|
||||
const totalPages = Math.ceil(unselectedUsers.length / usersPerPage)
|
||||
const startIdx = (currentPage - 1) * usersPerPage
|
||||
const paginatedUsers = unselectedUsers.slice(startIdx, startIdx + usersPerPage)
|
||||
@ -127,28 +130,28 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
return (
|
||||
<Modal
|
||||
id="invite-members-modal"
|
||||
title={directAdd ? 'Mitglieder hinzufügen' : 'Mitglieder einladen'}
|
||||
title={directAdd ? 'Spieler hinzufügen' : 'Spieler einladen'}
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
onSave={handleInvite}
|
||||
closeButtonColor={isSuccess ? "teal" : "blue"}
|
||||
closeButtonColor={isSuccess ? 'teal' : 'blue'}
|
||||
closeButtonTitle={
|
||||
isSuccess
|
||||
? directAdd ? 'Mitglieder hinzugefügt' : 'Einladungen versendet'
|
||||
? directAdd ? 'Spieler hinzugefügt' : 'Einladungen versendet'
|
||||
: directAdd ? 'Hinzufügen' : 'Einladungen senden'
|
||||
}
|
||||
>
|
||||
<p className="text-sm text-gray-700 dark:text-neutral-300 mb-2">
|
||||
{directAdd
|
||||
? 'Wähle Benutzer aus, die du direkt zum Team hinzufügen möchtest:'
|
||||
: 'Wähle Benutzer aus, die du in dein Team einladen möchtest:'}
|
||||
? 'Wähle Spieler aus, die du direkt zum Team hinzufügen möchtest:'
|
||||
: 'Wähle Spieler aus, die du in dein Team einladen möchtest:'}
|
||||
</p>
|
||||
|
||||
{/* Ausgewählte Benutzer anzeigen */}
|
||||
{selectedIds.length > 0 && (
|
||||
<>
|
||||
<div className="col-span-full">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 mb-2">
|
||||
Ausgewählte Mitglieder:
|
||||
Ausgewählte Spieler:
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-2">
|
||||
<AnimatePresence initial={false}>
|
||||
@ -159,9 +162,9 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
<motion.div
|
||||
key={user.steamId}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<MiniCard
|
||||
@ -175,6 +178,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
currentUserSteamId={steamId!}
|
||||
teamLeaderSteamId={team.leader}
|
||||
hideActions={true}
|
||||
rank={user.premierRank}
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
@ -182,8 +186,8 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suchen..."
|
||||
@ -191,6 +195,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="mt-2 w-full rounded border px-3 py-2 text-sm focus:outline-none focus:ring focus:ring-blue-400 dark:bg-neutral-800 dark:border-neutral-600 dark:text-neutral-100"
|
||||
/>
|
||||
|
||||
{isSuccess && (
|
||||
<div className="mt-2 px-4 py-2 text-sm text-green-700 bg-green-100 border border-green-200 rounded-lg">
|
||||
{directAdd
|
||||
@ -198,6 +203,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
: `${sentCount} Einladung${sentCount === 1 ? '' : 'en'} versendet!`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 mt-4">
|
||||
{isLoading ? (
|
||||
<LoadingSpinner />
|
||||
@ -212,15 +218,13 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
) : (
|
||||
<>
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
{paginatedUsers
|
||||
.filter((user) => !selectedIds.includes(user.steamId))
|
||||
.map((user) => (
|
||||
{!isSuccess && paginatedUsers.map((user) => (
|
||||
<motion.div
|
||||
key={user.steamId}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<MiniCard
|
||||
@ -238,7 +242,38 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
{isSuccess &&
|
||||
invitedIds.map((id) => {
|
||||
const user = allUsers.find((u) => u.steamId === id)
|
||||
if (!user) return null
|
||||
return (
|
||||
<motion.div
|
||||
key={`invited-${user.steamId}`}
|
||||
layout
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<MiniCard
|
||||
steamId={user.steamId}
|
||||
title={user.name}
|
||||
avatar={user.avatar}
|
||||
location={user.location}
|
||||
selected={false}
|
||||
draggable={false}
|
||||
currentUserSteamId={steamId!}
|
||||
teamLeaderSteamId={team.leader}
|
||||
hideActions={true}
|
||||
rank={user.premierRank}
|
||||
message="Eingeladen"
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
{ !isSuccess && (
|
||||
<div className="col-span-full flex justify-center mt-2">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
@ -246,6 +281,8 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
||||
onPageChange={(page) => setCurrentPage(page)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,7 @@ import Modal from './Modal'
|
||||
import MiniCard from './MiniCard'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { Player, Team } from '../types/team'
|
||||
import { useTeamManager } from '../hooks/useTeamManager'
|
||||
import { leaveTeam } from '../lib/team-actions'
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
@ -20,7 +20,6 @@ export default function LeaveTeamModal({ show, onClose, onSuccess, team }: Props
|
||||
|
||||
const [newLeaderId, setNewLeaderId] = useState<string>('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const { leaveTeam } = useTeamManager({}, null)
|
||||
|
||||
useEffect(() => {
|
||||
if (show && team.leader) {
|
||||
|
||||
@ -85,16 +85,16 @@ export function MatchDetails ({ match }: { match: Match }) {
|
||||
{sorted.map(p => (
|
||||
<Table.Row
|
||||
key={p.user.steamId}
|
||||
hoverable
|
||||
onClick={() => router.push(`/profile/${p.user.steamId}`)}
|
||||
>
|
||||
<Table.Cell className="py-1 flex items-center gap-2">
|
||||
<Table.Cell className="py-1 flex items-center gap-2" hoverable onClick={() => router.push(`/profile/${p.user.steamId}`)} >
|
||||
<img
|
||||
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
||||
alt={p.user.name}
|
||||
className="w-8 h-8 rounded-full"
|
||||
className="w-8 h-8 rounded-full mr-3"
|
||||
/>
|
||||
<div className='font-semibold text-base'>
|
||||
{p.user.name ?? 'Unbekannt'}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell>
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
// MiniCard.tsx
|
||||
'use client'
|
||||
|
||||
import Button from './Button'
|
||||
import Image from 'next/image'
|
||||
import PremierRankBadge from './PremierRankBadge'
|
||||
import { revokeInvitation } from '../lib/team-actions'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
|
||||
type MiniCardProps = {
|
||||
title: string
|
||||
@ -24,6 +27,9 @@ type MiniCardProps = {
|
||||
hideOverlay?: boolean
|
||||
isSelectable?: boolean
|
||||
isAdmin?: boolean
|
||||
message?: string
|
||||
isInvite?: boolean
|
||||
invitationId?: string
|
||||
}
|
||||
|
||||
export default function MiniCard({
|
||||
@ -46,6 +52,9 @@ export default function MiniCard({
|
||||
hideOverlay = false,
|
||||
isSelectable = true,
|
||||
isAdmin = false,
|
||||
message,
|
||||
isInvite = false,
|
||||
invitationId
|
||||
}: MiniCardProps) {
|
||||
//const isSelectable = typeof onSelect === 'function'
|
||||
const canEdit = (isAdmin || currentUserSteamId === teamLeaderSteamId) && steamId !== teamLeaderSteamId
|
||||
@ -65,6 +74,14 @@ export default function MiniCard({
|
||||
if (isSelectable) onSelect?.(steamId)
|
||||
}
|
||||
|
||||
const handleRevokeClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (invitationId) {
|
||||
revokeInvitation(invitationId)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKickClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@ -84,12 +101,12 @@ export default function MiniCard({
|
||||
return (
|
||||
<div className={`${cardClasses} group`} onClick={handleCardClick} {...dragListeners}>
|
||||
{canEdit && !hideActions && !hideOverlay && (
|
||||
<div className={`absolute inset-0 bg-white dark:bg-black bg-opacity-50 flex flex-col items-center justify-center gap-2 transition-opacity z-10 ${
|
||||
<div className={`absolute inset-0 bg-white dark:bg-black bg-opacity-80 flex flex-col items-center justify-center gap-2 transition-opacity z-10 ${
|
||||
hideOverlay ? 'opacity-0 pointer-events-none' : 'opacity-0 group-hover:opacity-100'
|
||||
}`}>
|
||||
<span className="text-gray-800 dark:text-neutral-200 font-semibold text-sm mb-1 truncate px-2 max-w-[90%] text-center">{title}</span>
|
||||
<div className="pointer-events-auto" onPointerDown={stopDrag}>
|
||||
<Button title="Kicken" color="red" variant="solid" size="sm" onClick={handleKickClick} />
|
||||
<Button className="max-w-[100px]" title={isInvite ? 'Einladung zurückziehen' : 'Kicken'} color="red" variant="solid" size={isInvite ? 'xs' : `sm`} onClick={isInvite ? handleRevokeClick : handleKickClick} />
|
||||
</div>
|
||||
{typeof onPromote === 'function' && (
|
||||
<div className="pointer-events-auto" onPointerDown={stopDrag}>
|
||||
@ -138,6 +155,21 @@ export default function MiniCard({
|
||||
<span className="text-xl mt-1" title="Weltweit">🌐</span>
|
||||
)}
|
||||
*/ }
|
||||
|
||||
{message && (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key="miniCardMessage"
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 20, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="absolute bottom-0 left-0 right-0 z-20 bg-green-700 text-white text-md text-center py-1 rounded-b-lg"
|
||||
>
|
||||
{message}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -9,7 +9,7 @@ export default function MiniCardDummy({ title, onClick, children }: MiniCardDumm
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`
|
||||
relative flex flex-col h-full max-h-[200px] items-center p-4 border border-dashed rounded-lg transition
|
||||
relative flex flex-col h-full max-h-[200px] max-w-[160px] items-center p-4 border border-dashed rounded-lg transition
|
||||
hover:border-blue-400 dark:hover:border-blue-400 hover:cursor-pointer
|
||||
border-gray-300 dark:border-neutral-700
|
||||
`}
|
||||
|
||||
@ -134,7 +134,7 @@ export default function Modal({
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-4 overflow-visible">{children}</div>
|
||||
<div className="p-4 overflow-y-auto max-h-[70vh] sm:max-h-[60vh]">{children}</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end items-center gap-x-2 py-3 px-4 border-t border-gray-200 dark:border-neutral-700">
|
||||
|
||||
@ -168,7 +168,7 @@ export default function Sidebar({ children }: { children?: React.ReactNode }) {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="sm:ml-64 flex-1 p-6 bg-white dark:bg-black overflow-y-auto">
|
||||
<main className="sm:ml-64 flex-1 h-screen p-6 bg-white dark:bg-black overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// SortableMiniCard.tsx
|
||||
'use client'
|
||||
|
||||
import { useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable'
|
||||
|
||||
@ -48,17 +48,21 @@ function Row({
|
||||
function Cell({
|
||||
children,
|
||||
as: Component = 'td',
|
||||
hoverable = false,
|
||||
className = '',
|
||||
...rest
|
||||
}: {
|
||||
children?: ReactNode
|
||||
as?: 'td' | 'th'
|
||||
hoverable?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
} & React.HTMLAttributes<HTMLTableCellElement>) {
|
||||
const hoverClass = hoverable ? 'hover:bg-gray-100 dark:hover:bg-neutral-700 cursor-pointer' : ''
|
||||
const baseClass =
|
||||
Component === 'th'
|
||||
? 'px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-400'
|
||||
: 'px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200'
|
||||
return <Component className={`${baseClass} ${className}`}>{children}</Component>
|
||||
? 'px-6 py-3 text-start font-medium text-xs text-gray-500 uppercase dark:text-neutral-400'
|
||||
: 'px-6 py-3 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200'
|
||||
return <Component {...rest} className={`${baseClass} ${hoverClass} ${className}`}>{children}</Component>
|
||||
}
|
||||
|
||||
// 📦 Zusammensetzen:
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
|
||||
import { useTeamManager } from '../hooks/useTeamManager'
|
||||
import TeamInvitationView from './TeamInvitationView'
|
||||
import TeamMemberView from './TeamMemberView'
|
||||
import NoTeamView from './NoTeamView'
|
||||
import LoadingSpinner from '@/app/components/LoadingSpinner'
|
||||
import CreateTeamButton from './CreateTeamButton'
|
||||
|
||||
import {
|
||||
acceptInvitation,
|
||||
rejectInvitation,
|
||||
markOneAsRead
|
||||
} from '@/app/lib/team-actions'
|
||||
import { Player, Team } from '../types/team'
|
||||
|
||||
type Props = {
|
||||
refetchKey?: string
|
||||
}
|
||||
@ -18,41 +24,86 @@ type Props = {
|
||||
function TeamCardComponent(props: Props, ref: any) {
|
||||
const { data: session } = useSession()
|
||||
const steamId = session?.user?.steamId ?? ''
|
||||
|
||||
const [refetchKey, setRefetchKey] = useState<string>()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const teamManager = useTeamManager({ ...props, refetchKey }, ref)
|
||||
const [team, setTeam] = useState<Team | null>(null)
|
||||
const [activePlayers, setActivePlayers] = useState<Player[]>([])
|
||||
const [inactivePlayers, setInactivePlayers] = useState<Player[]>([])
|
||||
const [invitedPlayers, setInvitedPlayers] = useState<Player[]>([])
|
||||
const [pendingInvitation, setPendingInvitation] = useState<any>(null)
|
||||
const [activeDragItem, setActiveDragItem] = useState<Player | null>(null)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [showLeaveModal, setShowLeaveModal] = useState(false)
|
||||
const [showInviteModal, setShowInviteModal] = useState(false)
|
||||
|
||||
// 1. Loading
|
||||
if (teamManager.isLoading) return <LoadingSpinner />
|
||||
const loadTeam = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/team')
|
||||
const data = await res.json()
|
||||
|
||||
// 2. Pending invitation
|
||||
if (
|
||||
!teamManager.team &&
|
||||
teamManager.pendingInvitation &&
|
||||
teamManager.pendingInvitation.type === 'team-invite'
|
||||
) {
|
||||
const notificationId = teamManager.pendingInvitation.id
|
||||
if (data.team) {
|
||||
setTeam(data.team)
|
||||
setActivePlayers(data.team.activePlayers || [])
|
||||
setInactivePlayers(data.team.inactivePlayers || [])
|
||||
setInvitedPlayers(data.team.invitedPlayers || [])
|
||||
} else {
|
||||
setTeam(null)
|
||||
}
|
||||
|
||||
// Einladung nur laden, wenn kein Team vorhanden
|
||||
if (!data.team) {
|
||||
const inviteRes = await fetch('/api/user/invitations')
|
||||
if (inviteRes.ok) {
|
||||
const inviteData = await inviteRes.json()
|
||||
const teamInvite = inviteData.invitations?.find(
|
||||
(i: any) => i.type === 'team-invite'
|
||||
)
|
||||
setPendingInvitation(teamInvite ?? null)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden des Teams:', err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMarkOneAsRead = async (id: string): Promise<void> => {
|
||||
await markOneAsRead(id)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadTeam()
|
||||
}, [refetchKey])
|
||||
|
||||
if (isLoading) return <LoadingSpinner />
|
||||
|
||||
// 1. Pending invitation
|
||||
if (!team && pendingInvitation) {
|
||||
const notificationId = pendingInvitation.id
|
||||
|
||||
return (
|
||||
<TeamInvitationView
|
||||
invitation={teamManager.pendingInvitation}
|
||||
invitation={pendingInvitation}
|
||||
notificationId={notificationId}
|
||||
onMarkAsRead={teamManager.markOneAsRead}
|
||||
onMarkAsRead={handleMarkOneAsRead}
|
||||
onAction={async (action, invitationId) => {
|
||||
if (action === 'accept') {
|
||||
await teamManager.acceptInvitation(invitationId)
|
||||
await acceptInvitation(invitationId)
|
||||
} else {
|
||||
await teamManager.rejectInvitation(invitationId)
|
||||
await rejectInvitation(invitationId)
|
||||
}
|
||||
await teamManager.markOneAsRead(notificationId)
|
||||
await markOneAsRead(notificationId)
|
||||
await loadTeam()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Kein Team → Hinweis + CreateTeamButton
|
||||
if (!teamManager.team) {
|
||||
// 2. Kein Team
|
||||
if (!team) {
|
||||
return (
|
||||
<div className="p-6 bg-white dark:bg-neutral-900 border rounded-lg dark:border-neutral-700 space-y-4">
|
||||
<NoTeamView />
|
||||
@ -63,7 +114,7 @@ function TeamCardComponent(props: Props, ref: any) {
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Team vorhanden → Member view
|
||||
// 3. Team vorhanden
|
||||
return (
|
||||
<div className="p-5 md:p-8 bg-white border border-gray-200 shadow-2xs rounded-xl dark:bg-neutral-800 dark:border-neutral-700">
|
||||
<div className="mb-4 xl:mb-8">
|
||||
@ -77,9 +128,21 @@ function TeamCardComponent(props: Props, ref: any) {
|
||||
|
||||
<form>
|
||||
<TeamMemberView
|
||||
{...teamManager}
|
||||
team={team}
|
||||
activePlayers={activePlayers}
|
||||
inactivePlayers={inactivePlayers}
|
||||
setactivePlayers={setActivePlayers}
|
||||
setInactivePlayers={setInactivePlayers}
|
||||
currentUserSteamId={steamId}
|
||||
adminMode={false}
|
||||
activeDragItem={activeDragItem}
|
||||
setActiveDragItem={setActiveDragItem}
|
||||
isDragging={isDragging}
|
||||
setIsDragging={setIsDragging}
|
||||
showLeaveModal={showLeaveModal}
|
||||
setShowLeaveModal={setShowLeaveModal}
|
||||
showInviteModal={showInviteModal}
|
||||
setShowInviteModal={setShowInviteModal}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// TeamMemberView.tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -14,7 +15,12 @@ import { Player, Team } from '../types/team'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useSSE } from '@/app/lib/useSSEStore'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useTeamManager } from '../hooks/useTeamManager'
|
||||
import {
|
||||
leaveTeam,
|
||||
reloadTeam,
|
||||
renameTeam,
|
||||
revokeInvitation,
|
||||
} from '@/app/lib/team-actions'
|
||||
import Button from './Button'
|
||||
import Image from 'next/image'
|
||||
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
||||
@ -38,6 +44,8 @@ type Props = {
|
||||
adminMode?: boolean
|
||||
}
|
||||
|
||||
type InvitedPlayer = Player & { invitationId: string }
|
||||
|
||||
export default function TeamMemberView({
|
||||
team,
|
||||
activePlayers,
|
||||
@ -64,7 +72,7 @@ export default function TeamMemberView({
|
||||
const canManage = adminMode || isLeader
|
||||
const canInvite = isLeader && !adminMode
|
||||
const canAddDirect = adminMode
|
||||
const { leaveTeam, reloadTeam, renameTeam, deleteTeam } = useTeamManager({}, null)
|
||||
//const { leaveTeam, reloadTeam, renameTeam, revokeInvitation } = useTeamManager({}, null)
|
||||
const [showRenameModal, setShowRenameModal] = useState(false)
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [isEditingName, setIsEditingName] = useState(false)
|
||||
@ -74,6 +82,7 @@ export default function TeamMemberView({
|
||||
const [logoFile, setLogoFile] = useState<File | null>(null)
|
||||
const [teamState, setTeamState] = useState<Team | null>(team)
|
||||
const [saveSuccess, setSaveSuccess] = useState(false)
|
||||
const [invitedPlayers, setInvitedPlayers] = useState<InvitedPlayer[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user?.steamId) {
|
||||
@ -85,6 +94,17 @@ export default function TeamMemberView({
|
||||
setTeamState(team)
|
||||
}, [team])
|
||||
|
||||
useEffect(() => {
|
||||
if (team?.invitedPlayers?.length) {
|
||||
const unique = Array.from(
|
||||
new Map(team.invitedPlayers.map(p => [p.steamId, p])).values()
|
||||
)
|
||||
setInvitedPlayers(unique.sort((a, b) => a.name.localeCompare(b.name)))
|
||||
} else {
|
||||
setInvitedPlayers([])
|
||||
}
|
||||
}, [team])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!source || !teamState?.id) return
|
||||
@ -108,12 +128,12 @@ export default function TeamMemberView({
|
||||
/* EIN Aufruf genügt – holt Team + Spieler + setzt States */
|
||||
fetch(`/api/team/${encodeURIComponent(data.teamId)}`)
|
||||
.then(r => r.json())
|
||||
.then(fresh => {
|
||||
setTeamState(fresh)
|
||||
setactivePlayers((fresh.activePlayers ?? [])
|
||||
.sort((a: Player, b: Player) => a.name.localeCompare(b.name)))
|
||||
setInactivePlayers((fresh.inactivePlayers ?? [])
|
||||
.sort((a: Player, b: Player) => a.name.localeCompare(b.name)))
|
||||
.then(({ team }) => {
|
||||
if (!team) return
|
||||
setTeamState(team)
|
||||
setactivePlayers(team.activePlayers.sort((a: Player, b: Player) => a.name.localeCompare(b.name)))
|
||||
setInactivePlayers(team.inactivePlayers.sort((a: Player, b: Player) => a.name.localeCompare(b.name)))
|
||||
setInvitedPlayers(team.invitedPlayers.sort((a: Player, b: Player) => a.name.localeCompare(b.name)))
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('SSE parse error:', err)
|
||||
@ -140,7 +160,6 @@ export default function TeamMemberView({
|
||||
}
|
||||
}, [source, teamState?.id, reloadTeam])
|
||||
|
||||
|
||||
const handleDragStart = (event: any) => {
|
||||
const id = event.active.id
|
||||
const item = activePlayers.find(p => p.steamId === id) || inactivePlayers.find(p => p.steamId === id)
|
||||
@ -210,6 +229,17 @@ export default function TeamMemberView({
|
||||
setTimeout(() => setSaveSuccess(false), 3000) // 3 Sekunden sichtbar
|
||||
}
|
||||
|
||||
const handleReload = async () => {
|
||||
if (!teamState?.id) return
|
||||
const updated = await reloadTeam(teamState.id)
|
||||
if (!updated) return
|
||||
|
||||
setTeamState(updated)
|
||||
setactivePlayers(updated.activePlayers)
|
||||
setInactivePlayers(updated.inactivePlayers)
|
||||
setInvitedPlayers(updated.invitedPlayers)
|
||||
}
|
||||
|
||||
const confirmKick = async () => {
|
||||
if (!kickCandidate) return
|
||||
|
||||
@ -243,7 +273,7 @@ export default function TeamMemberView({
|
||||
return
|
||||
}
|
||||
|
||||
await reloadTeam()
|
||||
await handleReload()
|
||||
} catch (err) {
|
||||
console.error('Fehler bei Leader-Übertragung:', err)
|
||||
}
|
||||
@ -252,7 +282,7 @@ export default function TeamMemberView({
|
||||
if (!teamState) return null
|
||||
if (!adminMode && !currentUserSteamId) return null
|
||||
|
||||
const manageSteam = adminMode ? teamState.leader : currentUserSteamId
|
||||
const manageSteam: string = adminMode ? teamState.leader ?? '' : currentUserSteamId
|
||||
|
||||
const renderMemberList = (players: Player[]) => (
|
||||
<AnimatePresence>
|
||||
@ -349,7 +379,7 @@ export default function TeamMemberView({
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
await reloadTeam()
|
||||
await handleReload()
|
||||
} else {
|
||||
alert('Fehler beim Hochladen des Logos.')
|
||||
}
|
||||
@ -379,7 +409,7 @@ export default function TeamMemberView({
|
||||
await renameTeam(teamState.id, editedName)
|
||||
setTeamState((prev) => prev ? { ...prev, teamname: editedName } : prev)
|
||||
setIsEditingName(false)
|
||||
await reloadTeam()
|
||||
await handleReload()
|
||||
}}
|
||||
className="h-[34px] px-3 flex items-center justify-center"
|
||||
>
|
||||
@ -491,17 +521,17 @@ export default function TeamMemberView({
|
||||
|
||||
<DndContext collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<div className="space-y-8">
|
||||
<DroppableZone id="active" label={`Aktives Team (${activePlayers.length} / 5)`} activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
||||
<DroppableZone id="active" label={`Aktive Spieler (${activePlayers.length} / 5)`} activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
||||
<SortableContext items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||
{renderMemberList(activePlayers)}
|
||||
</SortableContext>
|
||||
</DroppableZone>
|
||||
|
||||
<DroppableZone id="inactive" label="Inaktives Team" activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
||||
<DroppableZone id="inactive" label="Inaktive Spieler" activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
||||
<SortableContext items={inactivePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||
{renderMemberList(inactivePlayers)}
|
||||
{canManage && (
|
||||
<motion.div key="mini-card-dummy" initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} transition={{ duration: 0.2 }}>
|
||||
<motion.div key="mini-card-dummy" initial={{ opacity: 0 }} animate={{ opacity: 1}} exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
|
||||
<MiniCardDummy
|
||||
title={canAddDirect ? 'Hinzufügen' : 'Einladen'}
|
||||
onClick={() => {
|
||||
@ -519,6 +549,46 @@ export default function TeamMemberView({
|
||||
)}
|
||||
</SortableContext>
|
||||
</DroppableZone>
|
||||
|
||||
{invitedPlayers.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-md font-semibold text-gray-700 dark:text-gray-300">Eingeladene Spieler</h3>
|
||||
</div>
|
||||
<div className="w-full rounded-lg p-4 transition-colors min-h-[200px] border border-gray-300 dark:border-neutral-700">
|
||||
<div className="grid gap-4 justify-start grid-cols-[repeat(auto-fill,minmax(160px,1fr))]">
|
||||
<AnimatePresence>
|
||||
{invitedPlayers.map((player: Player) => (
|
||||
<motion.div
|
||||
key={player.steamId}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
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>
|
||||
|
||||
<DragOverlay>
|
||||
|
||||
@ -99,6 +99,7 @@ export function useTeamManager(
|
||||
logo: teamData.logo,
|
||||
activePlayers : newActive,
|
||||
inactivePlayers: newInactive,
|
||||
invitedPlayers : teamData.invitedPlayers ?? [],
|
||||
})
|
||||
setactivePlayers(newActive)
|
||||
setInactivePlayers(newInactive)
|
||||
@ -195,6 +196,22 @@ export function useTeamManager(
|
||||
}
|
||||
}
|
||||
|
||||
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' })
|
||||
@ -301,6 +318,7 @@ export function useTeamManager(
|
||||
setInactivePlayers,
|
||||
acceptInvitation,
|
||||
rejectInvitation,
|
||||
revokeInvitation,
|
||||
markAllAsRead,
|
||||
markOneAsRead,
|
||||
handleInviteAction,
|
||||
|
||||
202
src/app/lib/team-actions.ts
Normal file
202
src/app/lib/team-actions.ts
Normal file
@ -0,0 +1,202 @@
|
||||
// lib/team-actions.ts
|
||||
|
||||
import { Player, Team } from '../types/team'
|
||||
|
||||
// 🔄 Team laden
|
||||
export async function reloadTeam(teamId: string): Promise<Team | null> {
|
||||
try {
|
||||
const res = await fetch(`/api/team/${encodeURIComponent(teamId)}`)
|
||||
if (!res.ok) throw new Error('Fehler beim Abrufen des Teams')
|
||||
|
||||
const data = await res.json()
|
||||
const team = data.team ?? data
|
||||
if (!team) return null
|
||||
|
||||
const sortByName = (players: Player[]) =>
|
||||
players.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
logo: team.logo,
|
||||
leader: team.leader,
|
||||
activePlayers: sortByName(team.activePlayers ?? []),
|
||||
inactivePlayers: sortByName(team.inactivePlayers ?? []),
|
||||
invitedPlayers: sortByName(team.invitedPlayers ?? []),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('reloadTeam:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Einladung annehmen
|
||||
export async function acceptInvitation(invitationId: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch('/api/user/invitations/accept', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ invitationId }),
|
||||
})
|
||||
return res.ok
|
||||
} catch (error) {
|
||||
console.error('acceptInvitation:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Einladung ablehnen
|
||||
export async function rejectInvitation(invitationId: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch('/api/user/invitations/reject', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ invitationId }),
|
||||
})
|
||||
return res.ok
|
||||
} catch (error) {
|
||||
console.error('rejectInvitation:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 📩 Einladung zurückziehen
|
||||
export async function revokeInvitation(invitationId: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch('/api/user/invitations/revoke', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ invitationId }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.message || 'Fehler beim Zurückziehen der Einladung')
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('revokeInvitation:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ✏️ Team umbenennen
|
||||
export async function renameTeam(teamId: string, newName: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch('/api/team/rename', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ teamId, newName }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.message || 'Fehler beim Umbenennen')
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('renameTeam:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 🚪 Team verlassen
|
||||
export async function leaveTeam(steamId: string, newLeaderId?: string): Promise<boolean> {
|
||||
try {
|
||||
const body = newLeaderId ? { steamId, newLeaderId } : { steamId }
|
||||
|
||||
const res = await fetch('/api/team/leave', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.message || 'Fehler beim Verlassen')
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('leaveTeam:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 👢 Spieler kicken
|
||||
export async function kickPlayer(teamId: string, steamId: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch('/api/team/kick', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ teamId, steamId }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.message || 'Fehler beim Kicken')
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('kickPlayer:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 👑 Leader übertragen
|
||||
export async function transferLeader(teamId: string, newLeaderSteamId: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch('/api/team/transfer-leader', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ teamId, newLeaderSteamId }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.message || 'Fehler beim Übertragen der Leader-Rolle')
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('transferLeader:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 🗑️ Team löschen
|
||||
export async function deleteTeam(teamId: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch('/api/team/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ teamId }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.message || 'Fehler beim Löschen')
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('deleteTeam:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Einzelne Notification als gelesen markieren
|
||||
export async function markOneAsRead(id: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`/api/notifications/mark-read/${id}`, {
|
||||
method: 'POST',
|
||||
})
|
||||
return res.ok
|
||||
} catch (err) {
|
||||
console.error(`Fehler beim Markieren von Benachrichtigung ${id}:`, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -8,12 +8,17 @@ export type Player = {
|
||||
isAdmin?: boolean
|
||||
}
|
||||
|
||||
export type InvitedPlayer = Player & {
|
||||
invitationId: string
|
||||
}
|
||||
|
||||
|
||||
export type Team = {
|
||||
id: string
|
||||
name?: string | null
|
||||
logo?: string | null
|
||||
leader?: string | null
|
||||
activePlayers? : Player[]
|
||||
inactivePlayers?: Player[]
|
||||
invitedPlayers?: Player[]
|
||||
activePlayers: Player[]
|
||||
inactivePlayers: Player[]
|
||||
invitedPlayers: InvitedPlayer[]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user