update
This commit is contained in:
parent
e8e9632512
commit
63c6c9f87a
@ -8,16 +8,23 @@ export async function GET(
|
|||||||
{ params }: { params: { teamId: string } },
|
{ params }: { params: { teamId: string } },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
/* ─── 1) Team holen ─────────────────────────────── */
|
/* ─── 1) Team + Invites holen ───────────────────────────── */
|
||||||
const team = await prisma.team.findUnique({
|
const team = await prisma.team.findUnique({
|
||||||
where: { id: params.teamId },
|
where: { id: params.teamId },
|
||||||
|
include: {
|
||||||
|
invites: {
|
||||||
|
include: {
|
||||||
|
user: true, // ⬅ notwendig für eingeladenen Spieler
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!team) {
|
if (!team) {
|
||||||
return NextResponse.json({ error: 'Team nicht gefunden' }, { status: 404 })
|
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(
|
const allIds = Array.from(
|
||||||
new Set([...team.activePlayers, ...team.inactivePlayers]),
|
new Set([...team.activePlayers, ...team.inactivePlayers]),
|
||||||
)
|
)
|
||||||
@ -33,7 +40,6 @@ export async function GET(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/* Map steamId → Player */
|
|
||||||
const byId: Record<string, Player> = Object.fromEntries(
|
const byId: Record<string, Player> = Object.fromEntries(
|
||||||
users.map(u => [
|
users.map(u => [
|
||||||
u.steamId,
|
u.steamId,
|
||||||
@ -47,7 +53,6 @@ export async function GET(
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
/* ─── 3) Arrays umwandeln + sortieren ───────────── */
|
|
||||||
const activePlayers = team.activePlayers
|
const activePlayers = team.activePlayers
|
||||||
.map(id => byId[id])
|
.map(id => byId[id])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@ -58,7 +63,19 @@ export async function GET(
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.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 = {
|
const result = {
|
||||||
id : team.id,
|
id : team.id,
|
||||||
name : team.name,
|
name : team.name,
|
||||||
@ -67,6 +84,7 @@ export async function GET(
|
|||||||
createdAt : team.createdAt,
|
createdAt : team.createdAt,
|
||||||
activePlayers,
|
activePlayers,
|
||||||
inactivePlayers,
|
inactivePlayers,
|
||||||
|
invitedPlayers,
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(result)
|
return NextResponse.json(result)
|
||||||
|
|||||||
@ -3,15 +3,52 @@ import { prisma } from '@/app/lib/prisma'
|
|||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
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({
|
const allUsers = await prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
team: null,
|
team: null,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
steamId: true,
|
steamId : true,
|
||||||
name: true,
|
name : true,
|
||||||
avatar: true,
|
avatar : true,
|
||||||
location: true,
|
location : true,
|
||||||
premierRank: true,
|
premierRank: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
@ -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) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der verfügbaren Benutzer:', error)
|
console.error('Fehler beim Laden der verfügbaren Benutzer:', error)
|
||||||
return NextResponse.json({ message: 'Fehler beim Laden' }, { status: 500 })
|
return NextResponse.json({ message: 'Fehler beim Laden' }, { status: 500 })
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export async function POST(req: NextRequest) {
|
|||||||
/* Team holen */
|
/* Team holen */
|
||||||
const team = await prisma.team.findUnique({
|
const team = await prisma.team.findUnique({
|
||||||
where : { id: teamId },
|
where : { id: teamId },
|
||||||
select: { name: true },
|
select: { name: true, leader: true },
|
||||||
})
|
})
|
||||||
if (!team) {
|
if (!team) {
|
||||||
return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
|
return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
|
||||||
@ -62,6 +62,12 @@ export async function POST(req: NextRequest) {
|
|||||||
createdAt : notification.createdAt.toISOString(),
|
createdAt : notification.createdAt.toISOString(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await sendServerSSEMessage({
|
||||||
|
type: 'team-updated',
|
||||||
|
teamId,
|
||||||
|
targetUserIds: team.leader?.steamId,
|
||||||
|
})
|
||||||
|
|
||||||
return invite.id
|
return invite.id
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { getServerSession } from 'next-auth'
|
|||||||
import { baseAuthOptions } from '@/app/lib/auth'
|
import { baseAuthOptions } from '@/app/lib/auth'
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/app/lib/prisma'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
|
import type { InvitedPlayer } from '@/app/types/team'
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const session = await getServerSession(baseAuthOptions)
|
const session = await getServerSession(baseAuthOptions)
|
||||||
@ -14,25 +15,31 @@ export async function GET() {
|
|||||||
const team = await prisma.team.findFirst({
|
const team = await prisma.team.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{ leader: { steamId: session.user.steamId} },
|
{ leader: { steamId: session.user.steamId } },
|
||||||
{ activePlayers: { has: session.user.steamId } },
|
{ activePlayers: { has: session.user.steamId } },
|
||||||
{ inactivePlayers: { has: session.user.steamId } },
|
{ inactivePlayers: { has: session.user.steamId } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
leader: true,
|
||||||
matchesAsTeamA: {
|
matchesAsTeamA: {
|
||||||
include: {
|
include: {
|
||||||
teamA: true,
|
teamA: true,
|
||||||
teamB: true,
|
teamB: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
matchesAsTeamB: {
|
matchesAsTeamB: {
|
||||||
include: {
|
include: {
|
||||||
teamA: true,
|
teamA: true,
|
||||||
teamB: true,
|
teamB: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
invites: {
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!team) {
|
if (!team) {
|
||||||
@ -43,7 +50,13 @@ export async function GET() {
|
|||||||
|
|
||||||
const playerData = await prisma.user.findMany({
|
const playerData = await prisma.user.findMany({
|
||||||
where: { steamId: { in: steamIds } },
|
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
|
const activePlayers = team.activePlayers
|
||||||
@ -54,6 +67,25 @@ export async function GET() {
|
|||||||
.map((id) => playerData.find((m) => m.steamId === id))
|
.map((id) => playerData.find((m) => m.steamId === id))
|
||||||
.filter(Boolean)
|
.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]
|
const matches = [...team.matchesAsTeamA, ...team.matchesAsTeamB]
|
||||||
.filter(m => m.teamA && m.teamB)
|
.filter(m => m.teamA && m.teamB)
|
||||||
.sort((a, b) => new Date(a.matchDate).getTime() - new Date(b.matchDate).getTime())
|
.sort((a, b) => new Date(a.matchDate).getTime() - new Date(b.matchDate).getTime())
|
||||||
@ -63,9 +95,10 @@ export async function GET() {
|
|||||||
id: team.id,
|
id: team.id,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
logo: team.logo,
|
logo: team.logo,
|
||||||
leader: team.leaderId,
|
leader: team.leader?.steamId ?? null,
|
||||||
activePlayers,
|
activePlayers,
|
||||||
inactivePlayers,
|
inactivePlayers,
|
||||||
|
invitedPlayers,
|
||||||
matches,
|
matches,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,18 +5,49 @@ import { NextResponse, type NextRequest } from 'next/server'
|
|||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { teamId, activePlayers, inactivePlayers } = await req.json()
|
const { teamId, activePlayers, inactivePlayers, invitedPlayers } = await req.json()
|
||||||
|
|
||||||
if (!teamId || !Array.isArray(activePlayers) || !Array.isArray(inactivePlayers)) {
|
if (!teamId || !Array.isArray(activePlayers) || !Array.isArray(inactivePlayers)) {
|
||||||
return NextResponse.json({ error: 'Ungültige Eingabedaten' }, { status: 400 })
|
return NextResponse.json({ error: 'Ungültige Eingabedaten' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🟢 Team-Update: aktive & inaktive Spieler
|
||||||
await prisma.team.update({
|
await prisma.team.update({
|
||||||
where: { id: teamId },
|
where: { id: teamId },
|
||||||
data: { activePlayers, inactivePlayers },
|
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({
|
await sendServerSSEMessage({
|
||||||
type: 'team-updated',
|
type: 'team-updated',
|
||||||
|
|||||||
@ -133,6 +133,17 @@ export async function POST(
|
|||||||
return NextResponse.json({ message: 'Einladung abgelehnt' })
|
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 })
|
return NextResponse.json({ message: 'Ungültige Aktion' }, { status: 400 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler bei Einladung:', error)
|
console.error('Fehler bei Einladung:', error)
|
||||||
|
|||||||
@ -10,7 +10,7 @@ type ButtonProps = {
|
|||||||
modalId?: string
|
modalId?: string
|
||||||
color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent'
|
color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent'
|
||||||
variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link'
|
variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link'
|
||||||
size?: 'sm' | 'md' | 'lg'
|
size?: 'xs' |'sm' | 'md' | 'lg'
|
||||||
className?: string
|
className?: string
|
||||||
dropDirection?: "up" | "down" | "auto"
|
dropDirection?: "up" | "down" | "auto"
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@ -47,6 +47,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|||||||
: {}
|
: {}
|
||||||
|
|
||||||
const sizeClasses: Record<string, string> = {
|
const sizeClasses: Record<string, string> = {
|
||||||
|
xs: 'py-1 px-2',
|
||||||
sm: 'py-2 px-3',
|
sm: 'py-2 px-3',
|
||||||
md: 'py-3 px-4',
|
md: 'py-3 px-4',
|
||||||
lg: 'p-4 sm:p-5',
|
lg: 'p-4 sm:p-5',
|
||||||
|
|||||||
@ -59,12 +59,7 @@ export function DroppableZone({
|
|||||||
|
|
||||||
{/* Hier sitzt der Droppable-Ref */}
|
{/* Hier sitzt der Droppable-Ref */}
|
||||||
<div ref={setNodeRef} className={zoneClasses}>
|
<div ref={setNodeRef} className={zoneClasses}>
|
||||||
<div
|
<div className="grid gap-4 justify-start grid-cols-[repeat(auto-fill,minmax(160px,1fr))]">
|
||||||
className="
|
|
||||||
grid gap-4 justify-start
|
|
||||||
[grid-template-columns:repeat(5,minmax(0,160px))]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -23,49 +23,48 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
|
|
||||||
const [allUsers, setAllUsers] = useState<Player[]>([])
|
const [allUsers, setAllUsers] = useState<Player[]>([])
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
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 [isSuccess, setIsSuccess] = useState(false)
|
||||||
const [sentCount, setSentCount] = useState(0)
|
const [sentCount, setSentCount] = useState(0)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const usersPerPage = 9
|
const usersPerPage = 9
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show) {
|
if (show) {
|
||||||
fetchUsersNotInTeam()
|
fetchUsersNotInTeam()
|
||||||
setIsSuccess(false) // Status zurücksetzen beim Öffnen
|
setIsSuccess(false)
|
||||||
|
setInvitedIds([])
|
||||||
}
|
}
|
||||||
}, [show])
|
}, [show])
|
||||||
|
|
||||||
const fetchUsersNotInTeam = async () => {
|
const fetchUsersNotInTeam = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true)
|
||||||
const res = await fetch('/api/team/available-users')
|
const res = await fetch('/api/team/available-users')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setAllUsers(data.users || [])
|
setAllUsers(data.users || [])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Laden der Benutzer:', err)
|
console.error('Fehler beim Laden der Benutzer:', err)
|
||||||
}
|
} finally {
|
||||||
finally {
|
setIsLoading(false)
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (steamId: string) => {
|
const handleSelect = (steamId: string) => {
|
||||||
setSelectedIds((prev) =>
|
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 () => {
|
const handleInvite = async () => {
|
||||||
if (selectedIds.length === 0 || !steamId) return
|
if (selectedIds.length === 0 || !steamId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = directAdd ? '/api/team/add-players'
|
const url = directAdd ? '/api/team/add-players' : '/api/team/invite'
|
||||||
: '/api/team/invite'
|
|
||||||
const body = directAdd
|
const body = directAdd
|
||||||
? { teamId: team.id, steamIds: selectedIds }
|
? { teamId: team.id, steamIds: selectedIds }
|
||||||
: { teamId: team.id, userIds: selectedIds, invitedBy: steamId }
|
: { teamId: team.id, userIds: selectedIds, invitedBy: steamId }
|
||||||
@ -76,15 +75,15 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.json()
|
const error = await res.json()
|
||||||
console.error('Fehler beim Einladen:', error.message)
|
console.error('Fehler beim Einladen:', error.message)
|
||||||
} else {
|
} else {
|
||||||
setSentCount(selectedIds.length) // 👈 speichere Anzahl
|
setSentCount(selectedIds.length)
|
||||||
setIsSuccess(true) // ✅ Erfolg markieren
|
setInvitedIds(selectedIds) // 👈 Einladungsliste speichern
|
||||||
setSelectedIds([]) // Optional: Selektion leeren
|
setIsSuccess(true)
|
||||||
onSuccess() // ⚡ Nur Success-Callback, kein Schließen hier!
|
setSelectedIds([]) // ⛔ nicht zu früh löschen!
|
||||||
|
onSuccess()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Einladen:', err)
|
console.error('Fehler beim Einladen:', err)
|
||||||
@ -99,7 +98,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
window.HSOverlay.close(modalEl)
|
window.HSOverlay.close(modalEl)
|
||||||
}
|
}
|
||||||
onClose()
|
onClose()
|
||||||
}, 1500)
|
}, 2000)
|
||||||
|
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
@ -119,7 +118,11 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
return aSelected - bSelected
|
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 totalPages = Math.ceil(unselectedUsers.length / usersPerPage)
|
||||||
const startIdx = (currentPage - 1) * usersPerPage
|
const startIdx = (currentPage - 1) * usersPerPage
|
||||||
const paginatedUsers = unselectedUsers.slice(startIdx, startIdx + usersPerPage)
|
const paginatedUsers = unselectedUsers.slice(startIdx, startIdx + usersPerPage)
|
||||||
@ -127,28 +130,28 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
id="invite-members-modal"
|
id="invite-members-modal"
|
||||||
title={directAdd ? 'Mitglieder hinzufügen' : 'Mitglieder einladen'}
|
title={directAdd ? 'Spieler hinzufügen' : 'Spieler einladen'}
|
||||||
show={show}
|
show={show}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSave={handleInvite}
|
onSave={handleInvite}
|
||||||
closeButtonColor={isSuccess ? "teal" : "blue"}
|
closeButtonColor={isSuccess ? 'teal' : 'blue'}
|
||||||
closeButtonTitle={
|
closeButtonTitle={
|
||||||
isSuccess
|
isSuccess
|
||||||
? directAdd ? 'Mitglieder hinzugefügt' : 'Einladungen versendet'
|
? directAdd ? 'Spieler hinzugefügt' : 'Einladungen versendet'
|
||||||
: directAdd ? 'Hinzufügen' : 'Einladungen senden'
|
: directAdd ? 'Hinzufügen' : 'Einladungen senden'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p className="text-sm text-gray-700 dark:text-neutral-300 mb-2">
|
<p className="text-sm text-gray-700 dark:text-neutral-300 mb-2">
|
||||||
{directAdd
|
{directAdd
|
||||||
? 'Wähle Benutzer aus, die du direkt zum Team hinzufügen möchtest:'
|
? 'Wähle Spieler 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 in dein Team einladen möchtest:'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Ausgewählte Benutzer anzeigen */}
|
{/* Ausgewählte Benutzer anzeigen */}
|
||||||
{selectedIds.length > 0 && (
|
{selectedIds.length > 0 && (
|
||||||
<>
|
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 mb-2">
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 mb-2">
|
||||||
Ausgewählte Mitglieder:
|
Ausgewählte Spieler:
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-2">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-2">
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
@ -159,9 +162,9 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
<motion.div
|
<motion.div
|
||||||
key={user.steamId}
|
key={user.steamId}
|
||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
>
|
>
|
||||||
<MiniCard
|
<MiniCard
|
||||||
@ -175,6 +178,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
currentUserSteamId={steamId!}
|
currentUserSteamId={steamId!}
|
||||||
teamLeaderSteamId={team.leader}
|
teamLeaderSteamId={team.leader}
|
||||||
hideActions={true}
|
hideActions={true}
|
||||||
|
rank={user.premierRank}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
)
|
||||||
@ -182,8 +186,8 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Suchen..."
|
placeholder="Suchen..."
|
||||||
@ -191,6 +195,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
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"
|
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 && (
|
{isSuccess && (
|
||||||
<div className="mt-2 px-4 py-2 text-sm text-green-700 bg-green-100 border border-green-200 rounded-lg">
|
<div className="mt-2 px-4 py-2 text-sm text-green-700 bg-green-100 border border-green-200 rounded-lg">
|
||||||
{directAdd
|
{directAdd
|
||||||
@ -198,6 +203,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
: `${sentCount} Einladung${sentCount === 1 ? '' : 'en'} versendet!`}
|
: `${sentCount} Einladung${sentCount === 1 ? '' : 'en'} versendet!`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 mt-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 mt-4">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
@ -212,15 +218,13 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<AnimatePresence mode="popLayout" initial={false}>
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
{paginatedUsers
|
{!isSuccess && paginatedUsers.map((user) => (
|
||||||
.filter((user) => !selectedIds.includes(user.steamId))
|
|
||||||
.map((user) => (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
key={user.steamId}
|
key={user.steamId}
|
||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
>
|
>
|
||||||
<MiniCard
|
<MiniCard
|
||||||
@ -238,7 +242,38 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</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>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{ !isSuccess && (
|
||||||
<div className="col-span-full flex justify-center mt-2">
|
<div className="col-span-full flex justify-center mt-2">
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
@ -246,6 +281,8 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
onPageChange={(page) => setCurrentPage(page)}
|
onPageChange={(page) => setCurrentPage(page)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</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 { useTeamManager } from '../hooks/useTeamManager'
|
import { leaveTeam } from '../lib/team-actions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
show: boolean
|
show: boolean
|
||||||
@ -20,7 +20,6 @@ export default function LeaveTeamModal({ show, onClose, onSuccess, team }: Props
|
|||||||
|
|
||||||
const [newLeaderId, setNewLeaderId] = useState<string>('')
|
const [newLeaderId, setNewLeaderId] = useState<string>('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const { leaveTeam } = useTeamManager({}, null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show && team.leader) {
|
if (show && team.leader) {
|
||||||
|
|||||||
@ -85,16 +85,16 @@ export function MatchDetails ({ match }: { match: Match }) {
|
|||||||
{sorted.map(p => (
|
{sorted.map(p => (
|
||||||
<Table.Row
|
<Table.Row
|
||||||
key={p.user.steamId}
|
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
|
<img
|
||||||
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
||||||
alt={p.user.name}
|
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'}
|
{p.user.name ?? 'Unbekannt'}
|
||||||
|
</div>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
// MiniCard.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
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 { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
|
||||||
type MiniCardProps = {
|
type MiniCardProps = {
|
||||||
title: string
|
title: string
|
||||||
@ -24,6 +27,9 @@ type MiniCardProps = {
|
|||||||
hideOverlay?: boolean
|
hideOverlay?: boolean
|
||||||
isSelectable?: boolean
|
isSelectable?: boolean
|
||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
|
message?: string
|
||||||
|
isInvite?: boolean
|
||||||
|
invitationId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MiniCard({
|
export default function MiniCard({
|
||||||
@ -46,6 +52,9 @@ export default function MiniCard({
|
|||||||
hideOverlay = false,
|
hideOverlay = false,
|
||||||
isSelectable = true,
|
isSelectable = true,
|
||||||
isAdmin = false,
|
isAdmin = false,
|
||||||
|
message,
|
||||||
|
isInvite = false,
|
||||||
|
invitationId
|
||||||
}: MiniCardProps) {
|
}: MiniCardProps) {
|
||||||
//const isSelectable = typeof onSelect === 'function'
|
//const isSelectable = typeof onSelect === 'function'
|
||||||
const canEdit = (isAdmin || currentUserSteamId === teamLeaderSteamId) && steamId !== teamLeaderSteamId
|
const canEdit = (isAdmin || currentUserSteamId === teamLeaderSteamId) && steamId !== teamLeaderSteamId
|
||||||
@ -65,6 +74,14 @@ export default function MiniCard({
|
|||||||
if (isSelectable) onSelect?.(steamId)
|
if (isSelectable) onSelect?.(steamId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRevokeClick = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
if (invitationId) {
|
||||||
|
revokeInvitation(invitationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleKickClick = (e: React.MouseEvent) => {
|
const handleKickClick = (e: React.MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -84,12 +101,12 @@ export default function MiniCard({
|
|||||||
return (
|
return (
|
||||||
<div className={`${cardClasses} group`} onClick={handleCardClick} {...dragListeners}>
|
<div className={`${cardClasses} group`} onClick={handleCardClick} {...dragListeners}>
|
||||||
{canEdit && !hideActions && !hideOverlay && (
|
{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'
|
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>
|
<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}>
|
<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>
|
</div>
|
||||||
{typeof onPromote === 'function' && (
|
{typeof onPromote === 'function' && (
|
||||||
<div className="pointer-events-auto" onPointerDown={stopDrag}>
|
<div className="pointer-events-auto" onPointerDown={stopDrag}>
|
||||||
@ -138,6 +155,21 @@ export default function MiniCard({
|
|||||||
<span className="text-xl mt-1" title="Weltweit">🌐</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export default function MiniCardDummy({ title, onClick, children }: MiniCardDumm
|
|||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`
|
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
|
hover:border-blue-400 dark:hover:border-blue-400 hover:cursor-pointer
|
||||||
border-gray-300 dark:border-neutral-700
|
border-gray-300 dark:border-neutral-700
|
||||||
`}
|
`}
|
||||||
|
|||||||
@ -134,7 +134,7 @@ export default function Modal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body */}
|
{/* 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 */}
|
{/* Footer */}
|
||||||
<div className="flex justify-end items-center gap-x-2 py-3 px-4 border-t border-gray-200 dark:border-neutral-700">
|
<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>
|
</div>
|
||||||
</aside>
|
</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}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// SortableMiniCard.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable'
|
import { useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable'
|
||||||
|
|||||||
@ -48,17 +48,21 @@ function Row({
|
|||||||
function Cell({
|
function Cell({
|
||||||
children,
|
children,
|
||||||
as: Component = 'td',
|
as: Component = 'td',
|
||||||
|
hoverable = false,
|
||||||
className = '',
|
className = '',
|
||||||
|
...rest
|
||||||
}: {
|
}: {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
as?: 'td' | 'th'
|
as?: 'td' | 'th'
|
||||||
|
hoverable?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
} & React.HTMLAttributes<HTMLTableCellElement>) {
|
||||||
|
const hoverClass = hoverable ? 'hover:bg-gray-100 dark:hover:bg-neutral-700 cursor-pointer' : ''
|
||||||
const baseClass =
|
const baseClass =
|
||||||
Component === 'th'
|
Component === 'th'
|
||||||
? 'px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-400'
|
? 'px-6 py-3 text-start font-medium text-xs text-gray-500 uppercase dark:text-neutral-400'
|
||||||
: 'px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200'
|
: 'px-6 py-3 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200'
|
||||||
return <Component className={`${baseClass} ${className}`}>{children}</Component>
|
return <Component {...rest} className={`${baseClass} ${hoverClass} ${className}`}>{children}</Component>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📦 Zusammensetzen:
|
// 📦 Zusammensetzen:
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { forwardRef, useState } from 'react'
|
import { forwardRef, useEffect, useState } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
|
||||||
import { useTeamManager } from '../hooks/useTeamManager'
|
|
||||||
import TeamInvitationView from './TeamInvitationView'
|
import TeamInvitationView from './TeamInvitationView'
|
||||||
import TeamMemberView from './TeamMemberView'
|
import TeamMemberView from './TeamMemberView'
|
||||||
import NoTeamView from './NoTeamView'
|
import NoTeamView from './NoTeamView'
|
||||||
import LoadingSpinner from '@/app/components/LoadingSpinner'
|
import LoadingSpinner from '@/app/components/LoadingSpinner'
|
||||||
import CreateTeamButton from './CreateTeamButton'
|
import CreateTeamButton from './CreateTeamButton'
|
||||||
|
|
||||||
|
import {
|
||||||
|
acceptInvitation,
|
||||||
|
rejectInvitation,
|
||||||
|
markOneAsRead
|
||||||
|
} from '@/app/lib/team-actions'
|
||||||
|
import { Player, Team } from '../types/team'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
refetchKey?: string
|
refetchKey?: string
|
||||||
}
|
}
|
||||||
@ -18,41 +24,86 @@ type Props = {
|
|||||||
function TeamCardComponent(props: Props, ref: any) {
|
function TeamCardComponent(props: Props, ref: any) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const steamId = session?.user?.steamId ?? ''
|
const steamId = session?.user?.steamId ?? ''
|
||||||
|
|
||||||
const [refetchKey, setRefetchKey] = useState<string>()
|
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
|
const loadTeam = async () => {
|
||||||
if (teamManager.isLoading) return <LoadingSpinner />
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/team')
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
// 2. Pending invitation
|
if (data.team) {
|
||||||
if (
|
setTeam(data.team)
|
||||||
!teamManager.team &&
|
setActivePlayers(data.team.activePlayers || [])
|
||||||
teamManager.pendingInvitation &&
|
setInactivePlayers(data.team.inactivePlayers || [])
|
||||||
teamManager.pendingInvitation.type === 'team-invite'
|
setInvitedPlayers(data.team.invitedPlayers || [])
|
||||||
) {
|
} else {
|
||||||
const notificationId = teamManager.pendingInvitation.id
|
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 (
|
return (
|
||||||
<TeamInvitationView
|
<TeamInvitationView
|
||||||
invitation={teamManager.pendingInvitation}
|
invitation={pendingInvitation}
|
||||||
notificationId={notificationId}
|
notificationId={notificationId}
|
||||||
onMarkAsRead={teamManager.markOneAsRead}
|
onMarkAsRead={handleMarkOneAsRead}
|
||||||
onAction={async (action, invitationId) => {
|
onAction={async (action, invitationId) => {
|
||||||
if (action === 'accept') {
|
if (action === 'accept') {
|
||||||
await teamManager.acceptInvitation(invitationId)
|
await acceptInvitation(invitationId)
|
||||||
} else {
|
} else {
|
||||||
await teamManager.rejectInvitation(invitationId)
|
await rejectInvitation(invitationId)
|
||||||
}
|
}
|
||||||
await teamManager.markOneAsRead(notificationId)
|
await markOneAsRead(notificationId)
|
||||||
|
await loadTeam()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Kein Team → Hinweis + CreateTeamButton
|
// 2. Kein Team
|
||||||
if (!teamManager.team) {
|
if (!team) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 bg-white dark:bg-neutral-900 border rounded-lg dark:border-neutral-700 space-y-4">
|
<div className="p-6 bg-white dark:bg-neutral-900 border rounded-lg dark:border-neutral-700 space-y-4">
|
||||||
<NoTeamView />
|
<NoTeamView />
|
||||||
@ -63,7 +114,7 @@ function TeamCardComponent(props: Props, ref: any) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Team vorhanden → Member view
|
// 3. Team vorhanden
|
||||||
return (
|
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="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">
|
<div className="mb-4 xl:mb-8">
|
||||||
@ -77,9 +128,21 @@ function TeamCardComponent(props: Props, ref: any) {
|
|||||||
|
|
||||||
<form>
|
<form>
|
||||||
<TeamMemberView
|
<TeamMemberView
|
||||||
{...teamManager}
|
team={team}
|
||||||
|
activePlayers={activePlayers}
|
||||||
|
inactivePlayers={inactivePlayers}
|
||||||
|
setactivePlayers={setActivePlayers}
|
||||||
|
setInactivePlayers={setInactivePlayers}
|
||||||
currentUserSteamId={steamId}
|
currentUserSteamId={steamId}
|
||||||
adminMode={false}
|
adminMode={false}
|
||||||
|
activeDragItem={activeDragItem}
|
||||||
|
setActiveDragItem={setActiveDragItem}
|
||||||
|
isDragging={isDragging}
|
||||||
|
setIsDragging={setIsDragging}
|
||||||
|
showLeaveModal={showLeaveModal}
|
||||||
|
setShowLeaveModal={setShowLeaveModal}
|
||||||
|
showInviteModal={showInviteModal}
|
||||||
|
setShowInviteModal={setShowInviteModal}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// TeamMemberView.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@ -14,7 +15,12 @@ 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 { useSSE } from '@/app/lib/useSSEStore'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
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 Button from './Button'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
||||||
@ -38,6 +44,8 @@ type Props = {
|
|||||||
adminMode?: boolean
|
adminMode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvitedPlayer = Player & { invitationId: string }
|
||||||
|
|
||||||
export default function TeamMemberView({
|
export default function TeamMemberView({
|
||||||
team,
|
team,
|
||||||
activePlayers,
|
activePlayers,
|
||||||
@ -64,7 +72,7 @@ export default function TeamMemberView({
|
|||||||
const canManage = adminMode || isLeader
|
const canManage = adminMode || isLeader
|
||||||
const canInvite = isLeader && !adminMode
|
const canInvite = isLeader && !adminMode
|
||||||
const canAddDirect = adminMode
|
const canAddDirect = adminMode
|
||||||
const { leaveTeam, reloadTeam, renameTeam, deleteTeam } = useTeamManager({}, null)
|
//const { leaveTeam, reloadTeam, renameTeam, revokeInvitation } = useTeamManager({}, null)
|
||||||
const [showRenameModal, setShowRenameModal] = useState(false)
|
const [showRenameModal, setShowRenameModal] = useState(false)
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
const [isEditingName, setIsEditingName] = useState(false)
|
const [isEditingName, setIsEditingName] = useState(false)
|
||||||
@ -74,6 +82,7 @@ export default function TeamMemberView({
|
|||||||
const [logoFile, setLogoFile] = useState<File | 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[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session?.user?.steamId) {
|
if (session?.user?.steamId) {
|
||||||
@ -85,6 +94,17 @@ export default function TeamMemberView({
|
|||||||
setTeamState(team)
|
setTeamState(team)
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!source || !teamState?.id) return
|
if (!source || !teamState?.id) return
|
||||||
@ -108,12 +128,12 @@ export default function TeamMemberView({
|
|||||||
/* EIN Aufruf genügt – holt Team + Spieler + setzt States */
|
/* EIN Aufruf genügt – holt Team + Spieler + setzt States */
|
||||||
fetch(`/api/team/${encodeURIComponent(data.teamId)}`)
|
fetch(`/api/team/${encodeURIComponent(data.teamId)}`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(fresh => {
|
.then(({ team }) => {
|
||||||
setTeamState(fresh)
|
if (!team) return
|
||||||
setactivePlayers((fresh.activePlayers ?? [])
|
setTeamState(team)
|
||||||
.sort((a: Player, b: Player) => a.name.localeCompare(b.name)))
|
setactivePlayers(team.activePlayers.sort((a: Player, b: Player) => a.name.localeCompare(b.name)))
|
||||||
setInactivePlayers((fresh.inactivePlayers ?? [])
|
setInactivePlayers(team.inactivePlayers.sort((a: Player, b: Player) => a.name.localeCompare(b.name)))
|
||||||
.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) {
|
} catch (err) {
|
||||||
console.error('SSE parse error:', err)
|
console.error('SSE parse error:', err)
|
||||||
@ -140,7 +160,6 @@ export default function TeamMemberView({
|
|||||||
}
|
}
|
||||||
}, [source, teamState?.id, reloadTeam])
|
}, [source, teamState?.id, reloadTeam])
|
||||||
|
|
||||||
|
|
||||||
const handleDragStart = (event: any) => {
|
const handleDragStart = (event: any) => {
|
||||||
const id = event.active.id
|
const id = event.active.id
|
||||||
const item = activePlayers.find(p => p.steamId === id) || inactivePlayers.find(p => p.steamId === 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
|
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 () => {
|
const confirmKick = async () => {
|
||||||
if (!kickCandidate) return
|
if (!kickCandidate) return
|
||||||
|
|
||||||
@ -243,7 +273,7 @@ export default function TeamMemberView({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await reloadTeam()
|
await handleReload()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler bei Leader-Übertragung:', err)
|
console.error('Fehler bei Leader-Übertragung:', err)
|
||||||
}
|
}
|
||||||
@ -252,7 +282,7 @@ export default function TeamMemberView({
|
|||||||
if (!teamState) return null
|
if (!teamState) return null
|
||||||
if (!adminMode && !currentUserSteamId) return null
|
if (!adminMode && !currentUserSteamId) return null
|
||||||
|
|
||||||
const manageSteam = adminMode ? teamState.leader : currentUserSteamId
|
const manageSteam: string = adminMode ? teamState.leader ?? '' : currentUserSteamId
|
||||||
|
|
||||||
const renderMemberList = (players: Player[]) => (
|
const renderMemberList = (players: Player[]) => (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@ -349,7 +379,7 @@ export default function TeamMemberView({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await reloadTeam()
|
await handleReload()
|
||||||
} else {
|
} else {
|
||||||
alert('Fehler beim Hochladen des Logos.')
|
alert('Fehler beim Hochladen des Logos.')
|
||||||
}
|
}
|
||||||
@ -379,7 +409,7 @@ export default function TeamMemberView({
|
|||||||
await renameTeam(teamState.id, editedName)
|
await renameTeam(teamState.id, editedName)
|
||||||
setTeamState((prev) => prev ? { ...prev, teamname: editedName } : prev)
|
setTeamState((prev) => prev ? { ...prev, teamname: editedName } : prev)
|
||||||
setIsEditingName(false)
|
setIsEditingName(false)
|
||||||
await reloadTeam()
|
await handleReload()
|
||||||
}}
|
}}
|
||||||
className="h-[34px] px-3 flex items-center justify-center"
|
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}>
|
<DndContext collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||||
<div className="space-y-8">
|
<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}>
|
<SortableContext items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||||
{renderMemberList(activePlayers)}
|
{renderMemberList(activePlayers)}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DroppableZone>
|
</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}>
|
<SortableContext items={inactivePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||||
{renderMemberList(inactivePlayers)}
|
{renderMemberList(inactivePlayers)}
|
||||||
{canManage && (
|
{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
|
<MiniCardDummy
|
||||||
title={canAddDirect ? 'Hinzufügen' : 'Einladen'}
|
title={canAddDirect ? 'Hinzufügen' : 'Einladen'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -519,6 +549,46 @@ export default function TeamMemberView({
|
|||||||
)}
|
)}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DroppableZone>
|
</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>
|
</div>
|
||||||
|
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
|
|||||||
@ -99,6 +99,7 @@ export function useTeamManager(
|
|||||||
logo: teamData.logo,
|
logo: teamData.logo,
|
||||||
activePlayers : newActive,
|
activePlayers : newActive,
|
||||||
inactivePlayers: newInactive,
|
inactivePlayers: newInactive,
|
||||||
|
invitedPlayers : teamData.invitedPlayers ?? [],
|
||||||
})
|
})
|
||||||
setactivePlayers(newActive)
|
setactivePlayers(newActive)
|
||||||
setInactivePlayers(newInactive)
|
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 () => {
|
const markAllAsRead = async () => {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/notifications/mark-all-read', { method: 'POST' })
|
await fetch('/api/notifications/mark-all-read', { method: 'POST' })
|
||||||
@ -301,6 +318,7 @@ export function useTeamManager(
|
|||||||
setInactivePlayers,
|
setInactivePlayers,
|
||||||
acceptInvitation,
|
acceptInvitation,
|
||||||
rejectInvitation,
|
rejectInvitation,
|
||||||
|
revokeInvitation,
|
||||||
markAllAsRead,
|
markAllAsRead,
|
||||||
markOneAsRead,
|
markOneAsRead,
|
||||||
handleInviteAction,
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +1,24 @@
|
|||||||
// /types/team.ts
|
// /types/team.ts
|
||||||
export type Player = {
|
export type Player = {
|
||||||
steamId : string
|
steamId: string
|
||||||
name : string
|
name: string
|
||||||
avatar : string
|
avatar: string
|
||||||
location? : string
|
location?: string
|
||||||
premierRank?: number
|
premierRank?: number
|
||||||
isAdmin? : boolean
|
isAdmin?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InvitedPlayer = Player & {
|
||||||
|
invitationId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type Team = {
|
export type Team = {
|
||||||
id : string
|
id: string
|
||||||
name?: string | null
|
name?: string | null
|
||||||
logo?: string | null
|
logo?: string | null
|
||||||
leader?: string | null
|
leader?: string | null
|
||||||
activePlayers? : Player[]
|
activePlayers: Player[]
|
||||||
inactivePlayers?: Player[]
|
inactivePlayers: Player[]
|
||||||
invitedPlayers?: Player[]
|
invitedPlayers: InvitedPlayer[]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user