update
This commit is contained in:
parent
90a3bdeb35
commit
ad4fe7c29a
@ -17,7 +17,7 @@ export default function AdminPage() {
|
|||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'matches':
|
case 'matches':
|
||||||
return (
|
return (
|
||||||
<Card title="Matches">
|
<Card title="Matches" maxWidth='auto'>
|
||||||
<MatchesAdminManager />
|
<MatchesAdminManager />
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,45 +1,45 @@
|
|||||||
// /app/api/matches/route.ts
|
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/app/lib/prisma'
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
|
/* optionalen Query-Parameter lesen */
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
const matchType = searchParams.get('type') // z. B. "community"
|
||||||
|
|
||||||
|
/* falls übergeben ⇒ danach filtern */
|
||||||
const matches = await prisma.match.findMany({
|
const matches = await prisma.match.findMany({
|
||||||
|
where : matchType ? { matchType } : undefined,
|
||||||
orderBy: { demoDate: 'desc' },
|
orderBy: { demoDate: 'desc' },
|
||||||
include: {
|
include: {
|
||||||
teamA : true,
|
teamA : true,
|
||||||
teamB : true,
|
teamB : true,
|
||||||
players: {
|
players: { include: { user: true, stats: true, team: true } },
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
stats: true,
|
|
||||||
team: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const formatted = matches.map(match => ({
|
/* … rest bleibt unverändert … */
|
||||||
id: match.id,
|
const formatted = matches.map(m => ({
|
||||||
map: match.map,
|
id : m.id,
|
||||||
demoDate: match.demoDate,
|
map : m.map,
|
||||||
matchType: match.matchType,
|
demoDate: m.demoDate,
|
||||||
scoreA: match.scoreA,
|
matchType: m.matchType,
|
||||||
scoreB: match.scoreB,
|
scoreA : m.scoreA,
|
||||||
winnerTeam: match.winnerTeam ?? null,
|
scoreB : m.scoreB,
|
||||||
|
winnerTeam: m.winnerTeam ?? null,
|
||||||
teamA: {
|
teamA: {
|
||||||
id: match.teamA?.id ?? null,
|
id : m.teamA?.id ?? null,
|
||||||
name: match.teamA?.name ?? 'CT',
|
name: m.teamA?.name ?? 'CT',
|
||||||
logo: match.teamA?.logo ?? null,
|
logo: m.teamA?.logo ?? null,
|
||||||
score: match.scoreA,
|
score: m.scoreA,
|
||||||
},
|
},
|
||||||
teamB: {
|
teamB: {
|
||||||
id: match.teamB?.id ?? null,
|
id : m.teamB?.id ?? null,
|
||||||
name: match.teamB?.name ?? 'T',
|
name: m.teamB?.name ?? 'T',
|
||||||
logo: match.teamB?.logo ?? null,
|
logo: m.teamB?.logo ?? null,
|
||||||
score: match.scoreB,
|
score: m.scoreB,
|
||||||
},
|
},
|
||||||
players: match.players.map(p => ({
|
players: m.players.map(p => ({
|
||||||
steamId : p.steamId,
|
steamId : p.steamId,
|
||||||
name : p.user?.name,
|
name : p.user?.name,
|
||||||
avatar : p.user?.avatar,
|
avatar : p.user?.avatar,
|
||||||
@ -47,7 +47,7 @@ export async function GET() {
|
|||||||
teamId : p.teamId,
|
teamId : p.teamId,
|
||||||
teamName: p.team?.name ?? null,
|
teamName: p.team?.name ?? null,
|
||||||
})),
|
})),
|
||||||
}));
|
}))
|
||||||
|
|
||||||
return NextResponse.json(formatted)
|
return NextResponse.json(formatted)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,40 +1,30 @@
|
|||||||
// src/app/api/team/kick/route.ts
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { NextResponse, type NextRequest } from 'next/server'
|
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/app/lib/prisma'
|
||||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||||
|
import { removePlayerFromMatches } from '@/app/lib/removePlayerFromMatches'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
/* ------------------------------------------------------------------ *
|
/* ───────── 1) Payload prüfen ───────── */
|
||||||
* 1) Payload-Validierung *
|
|
||||||
* ------------------------------------------------------------------ */
|
|
||||||
const { teamId, steamId } = await req.json()
|
const { teamId, steamId } = await req.json()
|
||||||
|
|
||||||
if (!teamId || !steamId) {
|
if (!teamId || !steamId) {
|
||||||
return NextResponse.json({ message: 'Fehlende Daten' }, { status: 400 })
|
return NextResponse.json({ message: 'Fehlende Daten' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ *
|
/* ───────── 2) Team + User laden ─────── */
|
||||||
* 2) Team & User laden *
|
|
||||||
* ------------------------------------------------------------------ */
|
|
||||||
const team = await prisma.team.findUnique({ where: { id: teamId } })
|
const team = await prisma.team.findUnique({ where: { id: teamId } })
|
||||||
if (!team) {
|
if (!team) return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
|
||||||
return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where : { steamId },
|
where : { steamId },
|
||||||
select: { name: true },
|
select: { name: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
const userName = user?.name ?? 'Ein Mitglied'
|
const userName = user?.name ?? 'Ein Mitglied'
|
||||||
const teamName = team.name ?? 'Unbekanntes Team'
|
const teamName = team.name ?? 'Unbekanntes Team'
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ *
|
/* ───────── 3) Spieler aus Team-Arrays entfernen ───────── */
|
||||||
* 3) Spielerlisten aktualisieren *
|
|
||||||
* ------------------------------------------------------------------ */
|
|
||||||
const active = team.activePlayers.filter(id => id !== steamId)
|
const active = team.activePlayers.filter(id => id !== steamId)
|
||||||
const inactive = team.inactivePlayers.filter(id => id !== steamId)
|
const inactive = team.inactivePlayers.filter(id => id !== steamId)
|
||||||
|
|
||||||
@ -46,70 +36,63 @@ export async function POST(req: NextRequest) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/* der gekickte User gehört zu keinem Team mehr */
|
/* ───────── 4) User vom Team lösen ───────── */
|
||||||
await prisma.user.update({
|
await prisma.user.update({ where: { steamId }, data: { teamId: null } })
|
||||||
where: { steamId },
|
|
||||||
data : { teamId: null },
|
|
||||||
})
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ *
|
/* ───────── 5) Spieler aus offenen Matches werfen ───────── */
|
||||||
* 4) Notifikation für den gekickten User *
|
await removePlayerFromMatches(teamId, steamId)
|
||||||
* ------------------------------------------------------------------ */
|
|
||||||
const kickedNotification = await prisma.notification.create({
|
/* ───────── 6) Notifications & SSE ───────── */
|
||||||
|
|
||||||
|
/* an gekickten User */
|
||||||
|
const kickedN = await prisma.notification.create({
|
||||||
data: {
|
data: {
|
||||||
|
user : { connect: { steamId } },
|
||||||
title : 'Team verlassen',
|
title : 'Team verlassen',
|
||||||
message : `Du wurdest aus dem Team „${teamName}“ geworfen.`,
|
message : `Du wurdest aus dem Team „${teamName}“ geworfen.`,
|
||||||
actionType : 'team-kick',
|
actionType : 'team-kick',
|
||||||
actionData : null,
|
|
||||||
user : { connect: { steamId } }, // <-- Relation herstellen
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({
|
||||||
type : kickedNotification.actionType ?? 'notification',
|
type : kickedN.actionType ?? 'notification',
|
||||||
targetUserIds: [steamId],
|
targetUserIds: [steamId],
|
||||||
message : kickedNotification.message,
|
id : kickedN.id,
|
||||||
id : kickedNotification.id,
|
message : kickedN.message,
|
||||||
actionType : kickedNotification.actionType ?? undefined,
|
createdAt : kickedN.createdAt.toISOString(),
|
||||||
actionData : kickedNotification.actionData ?? undefined,
|
|
||||||
createdAt : kickedNotification.createdAt.toISOString(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ *
|
/* an verbleibende Mitglieder */
|
||||||
* 5) Notifikation für verbleibende Mitglieder *
|
const remaining = [...active, ...inactive]
|
||||||
* ------------------------------------------------------------------ */
|
|
||||||
const remainingUserIds = [...active, ...inactive]
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
remainingUserIds.map(async memberSteamId => {
|
remaining.map(async uid => {
|
||||||
const n = await prisma.notification.create({
|
const n = await prisma.notification.create({
|
||||||
data: {
|
data: {
|
||||||
|
user : { connect: { steamId: uid } },
|
||||||
title : 'Team-Update',
|
title : 'Team-Update',
|
||||||
message : `${userName} wurde aus dem Team „${teamName}“ geworfen.`,
|
message : `${userName} wurde aus dem Team „${teamName}“ gekickt.`,
|
||||||
actionType : 'team-kick-other',
|
actionType : 'team-kick-other',
|
||||||
actionData : null,
|
|
||||||
user : { connect: { steamId: memberSteamId } }, // <-- Relation
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({
|
||||||
type : n.actionType ?? 'notification',
|
type : n.actionType ?? 'notification',
|
||||||
targetUserIds: [memberSteamId],
|
targetUserIds: [uid],
|
||||||
message : n.message,
|
|
||||||
id : n.id,
|
id : n.id,
|
||||||
actionType : n.actionType ?? undefined,
|
message : n.message,
|
||||||
actionData : n.actionData ?? undefined,
|
|
||||||
createdAt : n.createdAt.toISOString(),
|
createdAt : n.createdAt.toISOString(),
|
||||||
})
|
})
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ *
|
/* ► UI neu laden lassen */
|
||||||
* 6) Erfolg *
|
await sendServerSSEMessage({
|
||||||
* ------------------------------------------------------------------ */
|
type : 'team-updated',
|
||||||
|
teamId,
|
||||||
|
targetUserIds : remaining,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({ message: 'Mitglied entfernt' })
|
return NextResponse.json({ message: 'Mitglied entfernt' })
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error('[KICK] Fehler:', error)
|
console.error('[KICK] Fehler:', err)
|
||||||
return NextResponse.json({ message: 'Serverfehler' }, { status: 500 })
|
return NextResponse.json({ message: 'Serverfehler' }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
import { NextResponse, type NextRequest } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/app/lib/prisma'
|
||||||
import { removePlayerFromTeam } from '@/app/lib/removePlayerFromTeam'
|
import { removePlayerFromTeam } from '@/app/lib/removePlayerFromTeam'
|
||||||
|
import { removePlayerFromMatches } from '@/app/lib/removePlayerFromMatches'
|
||||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { steamId } = await req.json()
|
const { steamId } = await req.json()
|
||||||
|
|
||||||
if (!steamId) {
|
if (!steamId) {
|
||||||
return NextResponse.json({ message: 'Steam ID fehlt' }, { status: 400 })
|
return NextResponse.json({ message: 'Steam-ID fehlt' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ───────── 1) Team ermitteln ───────── */
|
||||||
const team = await prisma.team.findFirst({
|
const team = await prisma.team.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
@ -19,105 +22,89 @@ export async function POST(req: NextRequest) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
if (!team) return NextResponse.json({ message: 'Kein Team gefunden' }, { status: 404 })
|
||||||
if (!team) {
|
|
||||||
return NextResponse.json({ message: 'Kein Team gefunden.' }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { activePlayers, inactivePlayers, leader } = removePlayerFromTeam(
|
const { activePlayers, inactivePlayers, leader } = removePlayerFromTeam(
|
||||||
{
|
{ activePlayers: team.activePlayers, inactivePlayers: team.inactivePlayers, leader: team.leaderId },
|
||||||
activePlayers: team.activePlayers,
|
steamId,
|
||||||
inactivePlayers: team.inactivePlayers,
|
)
|
||||||
leader: team.leaderId,
|
|
||||||
}, steamId)
|
|
||||||
|
|
||||||
|
/* ───────── 2) Team anpassen / löschen ───────── */
|
||||||
if (!leader) {
|
if (!leader) {
|
||||||
await prisma.team.delete({ where: { id: team.id } })
|
await prisma.team.delete({ where: { id: team.id } })
|
||||||
} else {
|
} else {
|
||||||
await prisma.team.update({
|
await prisma.team.update({
|
||||||
where: { id: team.id },
|
where: { id: team.id },
|
||||||
data : {
|
data : {
|
||||||
leader: {
|
leader: { connect: { steamId: leader } },
|
||||||
connect: { steamId: leader },
|
|
||||||
},
|
|
||||||
activePlayers,
|
activePlayers,
|
||||||
inactivePlayers,
|
inactivePlayers,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.user.update({
|
/* ───────── 3) User lösen ───────── */
|
||||||
where: { steamId },
|
await prisma.user.update({ where: { steamId }, data: { teamId: null } })
|
||||||
data: { teamId: null },
|
|
||||||
})
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
/* ───────── 4) Spieler aus Matches entfernen ───────── */
|
||||||
where: { steamId },
|
await removePlayerFromMatches(team.id, steamId)
|
||||||
select: { name: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
const notification = await prisma.notification.create({
|
/* ───────── 5) Notifications ───────── */
|
||||||
|
const user = await prisma.user.findUnique({ where: { steamId }, select: { name: true } })
|
||||||
|
const userName = user?.name ?? 'Ein Spieler'
|
||||||
|
const teamName = team.name ?? 'Dein Team'
|
||||||
|
const remaining = [...activePlayers, ...inactivePlayers].filter(id => id !== steamId)
|
||||||
|
|
||||||
|
/* an leavenden User */
|
||||||
|
const leaveN = await prisma.notification.create({
|
||||||
data: {
|
data: {
|
||||||
user: {
|
user : { connect: { steamId } },
|
||||||
connect: { steamId },
|
|
||||||
},
|
|
||||||
title : 'Teamupdate',
|
title : 'Teamupdate',
|
||||||
message: `Du hast das Team "${team.name}" verlassen.`,
|
message : `Du hast das Team „${teamName}“ verlassen.`,
|
||||||
actionType : 'team-left',
|
actionType : 'team-left',
|
||||||
actionData: null,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({
|
||||||
type: notification.actionType ?? 'notification',
|
type : leaveN.actionType ?? 'notification',
|
||||||
targetUserIds: [steamId],
|
targetUserIds: [steamId],
|
||||||
message: notification.message,
|
id : leaveN.id,
|
||||||
id: notification.id,
|
message : leaveN.message,
|
||||||
actionType: notification.actionType ?? undefined,
|
createdAt : leaveN.createdAt.toISOString(),
|
||||||
actionData: notification.actionData ?? undefined,
|
|
||||||
createdAt: notification.createdAt.toISOString(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const allRemainingPlayers = Array.from(new Set([
|
/* an verbleibende Mitglieder */
|
||||||
...activePlayers,
|
|
||||||
...inactivePlayers,
|
|
||||||
])).filter(id => id !== steamId)
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
allRemainingPlayers.map(async (userId) => {
|
remaining.map(async uid => {
|
||||||
const notification = await prisma.notification.create({
|
const n = await prisma.notification.create({
|
||||||
data: {
|
data: {
|
||||||
user: {
|
user : { connect: { steamId: uid } },
|
||||||
connect: { steamId: userId },
|
|
||||||
},
|
|
||||||
title : 'Teamupdate',
|
title : 'Teamupdate',
|
||||||
message: `${user?.name ?? 'Ein Spieler'} hat das Team verlassen.`,
|
message : `${userName} hat das Team verlassen.`,
|
||||||
actionType : 'team-member-left',
|
actionType : 'team-member-left',
|
||||||
actionData: null,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({
|
||||||
type: notification.actionType ?? 'notification',
|
type : n.actionType ?? 'notification',
|
||||||
targetUserIds: [userId],
|
targetUserIds: [uid],
|
||||||
message: notification.message,
|
id : n.id,
|
||||||
id: notification.id,
|
message : n.message,
|
||||||
actionType: notification.actionType ?? undefined,
|
createdAt : n.createdAt.toISOString(),
|
||||||
actionData: notification.actionData ?? undefined,
|
|
||||||
createdAt: notification.createdAt.toISOString(),
|
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ► UI neu laden lassen */
|
||||||
|
if (remaining.length) {
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({
|
||||||
type : 'team-updated',
|
type : 'team-updated',
|
||||||
teamId : team.id,
|
teamId : team.id,
|
||||||
targetUserIds: allRemainingPlayers,
|
targetUserIds : remaining,
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
)
|
|
||||||
|
|
||||||
return NextResponse.json({ message: 'Erfolgreich aus dem Team entfernt' })
|
return NextResponse.json({ message: 'Erfolgreich aus dem Team entfernt' })
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Verlassen des Teams:', error)
|
console.error('[LEAVE] Fehler:', err)
|
||||||
return NextResponse.json({ message: 'Fehler beim Verlassen des Teams' }, { status: 500 })
|
return NextResponse.json({ message: 'Serverfehler' }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,36 @@
|
|||||||
// /app/api/user/[steamId]/matches/route.ts
|
// /app/api/user/[steamId]/matches/route.ts
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse, type NextRequest } from 'next/server'
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/app/lib/prisma'
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_req: Request,
|
req : NextRequest, // ← Request wird gebraucht!
|
||||||
{ params }: { params: { steamId: string } }
|
{ params }: { params: { steamId: string } },
|
||||||
) {
|
) {
|
||||||
const steamId = params.steamId
|
const steamId = params.steamId
|
||||||
|
|
||||||
if (!steamId) {
|
if (!steamId) {
|
||||||
return NextResponse.json({ error: 'Steam-ID fehlt' }, { status: 400 })
|
return NextResponse.json({ error: 'Steam-ID fehlt' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ───────── Query-Parameter „types“ auslesen ───────── */
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
// ?types=premier,competitive
|
||||||
|
const typesParam = searchParams.get('types') // string | null
|
||||||
|
const types = typesParam
|
||||||
|
? typesParam.split(',').map(t => t.trim()).filter(Boolean)
|
||||||
|
: [] // leer ⇒ kein Filter
|
||||||
|
|
||||||
|
/* ───────── Daten holen ───────── */
|
||||||
try {
|
try {
|
||||||
const matchPlayers = await prisma.matchPlayer.findMany({
|
const matchPlayers = await prisma.matchPlayer.findMany({
|
||||||
where: { steamId },
|
where: {
|
||||||
|
steamId,
|
||||||
|
|
||||||
|
/* nur wenn Filter gesetzt ist */
|
||||||
|
...(types.length && {
|
||||||
|
match: { matchType: { in: types } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
select: {
|
select: {
|
||||||
teamId: true,
|
teamId: true,
|
||||||
team : true,
|
team : true,
|
||||||
@ -29,63 +45,63 @@ export async function GET(
|
|||||||
matchType : true,
|
matchType : true,
|
||||||
teamAId : true,
|
teamAId : true,
|
||||||
teamBId : true,
|
teamBId : true,
|
||||||
teamAUsers: true,
|
teamAUsers : { select: { steamId: true } },
|
||||||
teamBUsers: true,
|
teamBUsers : { select: { steamId: true } },
|
||||||
winnerTeam : true,
|
winnerTeam : true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stats: true,
|
stats: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
|
||||||
match: {
|
orderBy: { match: { demoDate: 'desc' } },
|
||||||
demoDate: 'desc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = matchPlayers.map((mp) => {
|
/* ───────── Aufbereiten fürs Frontend ───────── */
|
||||||
const match = mp.match
|
const data = matchPlayers.map(mp => {
|
||||||
|
const m = mp.match
|
||||||
const stats = mp.stats
|
const stats = mp.stats
|
||||||
|
|
||||||
const kills = stats?.kills ?? 0
|
const kills = stats?.kills ?? 0
|
||||||
const deaths = stats?.deaths ?? 0
|
const deaths = stats?.deaths ?? 0
|
||||||
const kdr = deaths > 0 ? (kills / deaths).toFixed(2) : '∞'
|
const kdr = deaths ? (kills / deaths).toFixed(2) : '∞'
|
||||||
const roundCount = match.roundCount
|
|
||||||
const rankOld = stats?.rankOld ?? null
|
const rankOld = stats?.rankOld ?? null
|
||||||
const rankNew = stats?.rankNew ?? null
|
const rankNew = stats?.rankNew ?? null
|
||||||
|
|
||||||
const rankChange =
|
const rankChange =
|
||||||
typeof rankNew === 'number' && typeof rankOld === 'number'
|
rankNew != null && rankOld != null ? rankNew - rankOld : null
|
||||||
? rankNew - rankOld
|
|
||||||
: null
|
|
||||||
const matchType = match.matchType ?? 'community'
|
|
||||||
|
|
||||||
const isInTeamA = match.teamAUsers.some((user) => user.steamId === steamId)
|
/* Team des Spielers ermitteln */
|
||||||
const playerTeam = isInTeamA ? 'CT' : 'T'
|
const playerTeam =
|
||||||
|
m.teamAUsers.some(u => u.steamId === steamId) ? 'CT' : 'T'
|
||||||
|
|
||||||
const scoreCT = match.scoreA ?? 0
|
const score = `${m.scoreA ?? 0} : ${m.scoreB ?? 0}`
|
||||||
const scoreT = match.scoreB ?? 0
|
|
||||||
const score = `${scoreCT} : ${scoreT}`
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: match.id,
|
id : m.id,
|
||||||
map: match.map ?? 'Unknown',
|
map : m.map ?? 'Unknown',
|
||||||
date: match.demoDate,
|
date : m.demoDate?.toISOString() ?? '',
|
||||||
matchType,
|
matchType : m.matchType ?? 'community',
|
||||||
|
|
||||||
score,
|
score,
|
||||||
roundCount,
|
roundCount: m.roundCount,
|
||||||
|
|
||||||
rankOld,
|
rankOld,
|
||||||
rankNew,
|
rankNew,
|
||||||
rankChange,
|
rankChange,
|
||||||
|
|
||||||
kills,
|
kills,
|
||||||
deaths,
|
deaths,
|
||||||
kdr,
|
kdr,
|
||||||
winnerTeam: match.winnerTeam ?? null,
|
|
||||||
team: playerTeam,
|
winnerTeam: m.winnerTeam ?? null,
|
||||||
|
team : playerTeam, // „CT“ oder „T“
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.json(data)
|
return NextResponse.json(data)
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error('[API] Fehler beim Laden der Matches:', error)
|
console.error('[API] Fehler beim Laden der Matches:', err)
|
||||||
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
197
src/app/components/CommunityMatchList.tsx
Normal file
197
src/app/components/CommunityMatchList.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import { de } from 'date-fns/locale'
|
||||||
|
import Switch from '@/app/components/Switch'
|
||||||
|
import Button from './Button'
|
||||||
|
import { Match } from '../types/match'
|
||||||
|
import { differenceInMinutes } from 'date-fns'
|
||||||
|
|
||||||
|
type Props = { matchType?: string }
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ */
|
||||||
|
/* Helpers */
|
||||||
|
/* ------------------------------------------------------------ */
|
||||||
|
const getTeamLogo = (logo?: string | null) =>
|
||||||
|
logo ? `/assets/img/logos/${logo}` : '/assets/img/logos/cs2.webp'
|
||||||
|
|
||||||
|
const toDateKey = (d: Date) => d.toISOString().slice(0, 10)
|
||||||
|
const weekdayDE = new Intl.DateTimeFormat('de-DE', { weekday: 'long' })
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ */
|
||||||
|
/* Component */
|
||||||
|
/* ------------------------------------------------------------ */
|
||||||
|
export default function CommunityMatchList({ matchType }: Props) {
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [matches, setMatches] = useState<Match[]>([])
|
||||||
|
const [onlyOwn, setOnlyOwn] = useState(false)
|
||||||
|
|
||||||
|
/* Daten laden */
|
||||||
|
useEffect(() => {
|
||||||
|
const url = `/api/matches${matchType ? `?type=${encodeURIComponent(matchType)}` : ''}`
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(r => (r.ok ? r.json() : []))
|
||||||
|
.then(setMatches)
|
||||||
|
.catch(err => console.error('[MatchList] Laden fehlgeschlagen:', err))
|
||||||
|
}, [matchType])
|
||||||
|
|
||||||
|
/* Sortieren + Gruppieren (ohne vorher zu filtern!) */
|
||||||
|
const grouped = (() => {
|
||||||
|
const sorted = [...matches].sort(
|
||||||
|
(a, b) => new Date(a.demoDate).getTime() - new Date(b.demoDate).getTime(),
|
||||||
|
)
|
||||||
|
|
||||||
|
const map = new Map<string, Match[]>()
|
||||||
|
for (const m of sorted) {
|
||||||
|
const key = toDateKey(new Date(m.demoDate))
|
||||||
|
map.set(key, [...(map.get(key) ?? []), m])
|
||||||
|
}
|
||||||
|
return Array.from(map.entries()) // [ [ '2025-08-28', [ … ] ], … ]
|
||||||
|
})()
|
||||||
|
|
||||||
|
/* Render */
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto py-8 px-4 space-y-6">
|
||||||
|
{/* Kopfzeile ----------------------------------------------------- */}
|
||||||
|
<div className="flex items-center justify-between flex-wrap gap-y-4">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-700 dark:text-neutral-300">
|
||||||
|
Geplante Matches
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Switch
|
||||||
|
id="only-own-team"
|
||||||
|
checked={onlyOwn}
|
||||||
|
onChange={setOnlyOwn}
|
||||||
|
labelRight="Nur mein Team anzeigen"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{session?.user?.isAdmin && (
|
||||||
|
<Link href="/admin/matches">
|
||||||
|
<Button color="blue" onClick={() => router.push(`/admin/matches`)}>Match erstellen</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Inhalt ------------------------------------------------------- */}
|
||||||
|
{grouped.length === 0 ? (
|
||||||
|
<p className="text-gray-700 dark:text-neutral-300">Keine Matches geplant.</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{grouped.map(([dateKey, dayMatches], dayIdx) => {
|
||||||
|
const dateObj = new Date(dateKey + 'T00:00:00')
|
||||||
|
const dayLabel = `Tag #${dayIdx + 1} – ${weekdayDE.format(dateObj)}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={dateKey} className="flex flex-col gap-4">
|
||||||
|
{/* Tages-Header */}
|
||||||
|
<div className="bg-yellow-300 dark:bg-yellow-500 text-center py-2 font-bold tracking-wider">
|
||||||
|
{dayLabel}<br />
|
||||||
|
{dateKey}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Matches des Tages */}
|
||||||
|
{dayMatches.map(m => {
|
||||||
|
/* 1️⃣ Regeln --------------------------------------------- */
|
||||||
|
const demoDate = new Date(m.demoDate)
|
||||||
|
const started = demoDate <= Date.now()
|
||||||
|
const unfinished = !m.winnerTeam && m.scoreA == null && m.scoreB == null
|
||||||
|
const isLive = started && unfinished // ← live-Flag
|
||||||
|
|
||||||
|
const isOwnTeam =
|
||||||
|
session?.user?.team &&
|
||||||
|
(m.teamA.id === session.user.team || m.teamB.id === session.user.team)
|
||||||
|
|
||||||
|
/* Wenn nur-Own aktiv & nicht eigenes Match → abdunkeln */
|
||||||
|
const dimmed = onlyOwn && !isOwnTeam
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={m.id}
|
||||||
|
href={`/match-details/${m.id}`}
|
||||||
|
className={`
|
||||||
|
flex flex-col items-center gap-4 bg-neutral-300 dark:bg-neutral-800
|
||||||
|
text-gray-800 dark:text-white rounded-sm py-4
|
||||||
|
hover:scale-105 hover:bg-neutral-400 hover:dark:bg-neutral-700
|
||||||
|
hover:shadow-md transition-transform h-[172px]
|
||||||
|
${dimmed ? 'opacity-40' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/** ⏱ kleine Live-Marke, falls gewünscht */}
|
||||||
|
{isLive && (
|
||||||
|
<span className="absolute px-2 py-0.5 text-xs font-semibold rounded-full bg-red-600 text-white">
|
||||||
|
LIVE
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Teams -------------------------------------------------- */}
|
||||||
|
<div className="flex w-full justify-around items-center">
|
||||||
|
{/* Team A */}
|
||||||
|
<div className="flex flex-col items-center w-1/3">
|
||||||
|
<Image
|
||||||
|
src={getTeamLogo(m.teamA.logo)}
|
||||||
|
alt={m.teamA.name}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
className="rounded-full border bg-white"
|
||||||
|
/>
|
||||||
|
<span className="mt-2 text-xs">{m.teamA.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* vs */}
|
||||||
|
<span className="font-bold">vs</span>
|
||||||
|
|
||||||
|
{/* Team B */}
|
||||||
|
<div className="flex flex-col items-center w-1/3">
|
||||||
|
<Image
|
||||||
|
src={getTeamLogo(m.teamB.logo)}
|
||||||
|
alt={m.teamB.name}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
className="rounded-full border bg-white"
|
||||||
|
/>
|
||||||
|
<span className="mt-2 text-xs">{m.teamB.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Datum + Uhrzeit --------------------------------------- */}
|
||||||
|
<div className="flex flex-col items-center space-y-1 mt-2">
|
||||||
|
{/* Datum */}
|
||||||
|
<span className={`px-3 py-0.5 rounded-full text-sm font-semibold ${isLive ? 'bg-red-300 dark:bg-red-500' : 'bg-yellow-300 dark:bg-yellow-500'}`}>
|
||||||
|
{format(new Date(m.demoDate), 'dd.MM.yyyy', { locale: de })}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Zeit */}
|
||||||
|
<span className="flex items-center gap-1 text-xs opacity-80">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-3.5 h-3.5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
>
|
||||||
|
<path d="M256 48a208 208 0 1 0 208 208A208.24 208.24 0 0 0 256 48Zm0 384a176 176 0 1 1 176-176 176.2 176.2 0 0 1-176 176Zm80-176h-64V144a16 16 0 0 0-32 0v120a16 16 0 0 0 16 16h80a16 16 0 0 0 0-32Z" />
|
||||||
|
</svg>
|
||||||
|
{format(new Date(m.demoDate), 'HH:mm', { locale: de })} Uhr
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -9,12 +9,14 @@ type DroppableZoneProps = {
|
|||||||
label: string
|
label: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
activeDragItem: Player | null
|
activeDragItem: Player | null
|
||||||
|
saveSuccess?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DroppableZone({
|
export function DroppableZone({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
children,
|
children,
|
||||||
|
saveSuccess = false,
|
||||||
}: DroppableZoneProps) {
|
}: DroppableZoneProps) {
|
||||||
const { isOver, setNodeRef } = useDroppable({ id })
|
const { isOver, setNodeRef } = useDroppable({ id })
|
||||||
|
|
||||||
@ -31,10 +33,30 @@ export function DroppableZone({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<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">
|
<h3 className="text-md font-semibold text-gray-700 dark:text-gray-300">
|
||||||
{label}
|
{label}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{saveSuccess && (
|
||||||
|
<div className="flex items-center gap-1 text-green-600 text-sm font-medium">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Änderungen gespeichert!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Hier sitzt der Droppable-Ref */}
|
{/* Hier sitzt der Droppable-Ref */}
|
||||||
<div ref={setNodeRef} className={zoneClasses}>
|
<div ref={setNodeRef} className={zoneClasses}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import EditMatchPlayersModal from './EditMatchPlayersModal'
|
|||||||
import type { EditSide } from './EditMatchPlayersModal' // 'A' | 'B'
|
import type { EditSide } from './EditMatchPlayersModal' // 'A' | 'B'
|
||||||
|
|
||||||
import type { Match, MatchPlayer } from '../types/match'
|
import type { Match, MatchPlayer } from '../types/match'
|
||||||
|
import Button from './Button'
|
||||||
|
|
||||||
/* ─────────────────── Hilfsfunktionen ────────────────────────── */
|
/* ─────────────────── Hilfsfunktionen ────────────────────────── */
|
||||||
const kdr = (k?: number, d?: number) =>
|
const kdr = (k?: number, d?: number) =>
|
||||||
@ -165,13 +166,14 @@ export function MatchDetails ({ match }: { match: Match }) {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{canEditA && isFutureMatch && (
|
{canEditA && isFutureMatch && (
|
||||||
<button
|
<Button
|
||||||
|
size='sm'
|
||||||
onClick={() => setEditSide('A')}
|
onClick={() => setEditSide('A')}
|
||||||
className="px-3 py-1.5 text-sm rounded-lg
|
className="px-3 py-1.5 text-sm rounded-lg
|
||||||
bg-blue-600 hover:bg-blue-700 text-white"
|
bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
>
|
>
|
||||||
Spieler bearbeiten
|
Spieler bearbeiten
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -186,13 +188,14 @@ export function MatchDetails ({ match }: { match: Match }) {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{canEditB && isFutureMatch && (
|
{canEditB && isFutureMatch && (
|
||||||
<button
|
<Button
|
||||||
|
size='sm'
|
||||||
onClick={() => setEditSide('B')}
|
onClick={() => setEditSide('B')}
|
||||||
className="px-3 py-1.5 text-sm rounded-lg
|
className="px-3 py-1.5 text-sm rounded-lg
|
||||||
bg-blue-600 hover:bg-blue-700 text-white"
|
bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
>
|
>
|
||||||
Spieler bearbeiten
|
Spieler bearbeiten
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import Link from 'next/link'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useSession } from 'next-auth/react'
|
|
||||||
import Switch from '@/app/components/Switch'
|
|
||||||
|
|
||||||
type Match = {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description?: string
|
|
||||||
matchDate: string
|
|
||||||
teamA: { id: string; teamname: string; logo?: string | null }
|
|
||||||
teamB: { id: string; teamname: string; logo?: string | null }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTeamLogo(logo?: string | null) {
|
|
||||||
return logo ? `/assets/img/logos/${logo}` : '/default-logo.png'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MatchList() {
|
|
||||||
const { data: session } = useSession()
|
|
||||||
const [matches, setMatches] = useState<Match[]>([])
|
|
||||||
const [onlyOwnTeam, setOnlyOwnTeam] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/api/matches')
|
|
||||||
.then((res) => res.ok ? res.json() : [])
|
|
||||||
.then(setMatches)
|
|
||||||
.catch((err) => console.error('Fehler beim Laden der Matches:', err))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const filteredMatches = onlyOwnTeam && session?.user?.team
|
|
||||||
? matches.filter(m =>
|
|
||||||
m.teamA.id === session.user.team || m.teamB.id === session.user.team
|
|
||||||
)
|
|
||||||
: matches
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4 space-y-4">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h1 className="text-2xl font-bold">Geplante Matches</h1>
|
|
||||||
{session?.user?.team && (
|
|
||||||
<Switch
|
|
||||||
id="only-own-team"
|
|
||||||
checked={onlyOwnTeam}
|
|
||||||
onChange={setOnlyOwnTeam}
|
|
||||||
labelRight="Nur mein Team"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filteredMatches.length === 0 ? (
|
|
||||||
<p className="text-gray-500">Keine Matches geplant.</p>
|
|
||||||
) : (
|
|
||||||
<ul className="space-y-4">
|
|
||||||
{filteredMatches.map((match) => (
|
|
||||||
<li key={match.id}>
|
|
||||||
<Link
|
|
||||||
href={`/matches/${match.id}`}
|
|
||||||
className="block border rounded p-4 hover:bg-gray-50 dark:hover:bg-neutral-800 transition"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between text-center">
|
|
||||||
{/* Team A */}
|
|
||||||
<div className="flex flex-col items-center w-1/4">
|
|
||||||
<Image
|
|
||||||
src={getTeamLogo(match.teamA.logo)}
|
|
||||||
alt={match.teamA.teamname}
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="rounded-full border object-cover bg-white"
|
|
||||||
/>
|
|
||||||
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
|
|
||||||
{match.teamA.teamname}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Datum / Zeit */}
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300 w-1/2">
|
|
||||||
<div>{new Date(match.matchDate).toLocaleDateString('de-DE')}</div>
|
|
||||||
<div>{new Date(match.matchDate).toLocaleTimeString('de-DE', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})} Uhr</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Team B */}
|
|
||||||
<div className="flex flex-col items-center w-1/4">
|
|
||||||
<Image
|
|
||||||
src={getTeamLogo(match.teamB.logo)}
|
|
||||||
alt={match.teamB.teamname}
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="rounded-full border object-cover bg-white"
|
|
||||||
/>
|
|
||||||
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
|
|
||||||
{match.teamB.teamname}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Match-Titel */}
|
|
||||||
<div className="mt-3 text-sm font-medium text-center text-gray-700 dark:text-gray-300">
|
|
||||||
{match.title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Match-Beschreibung (optional) */}
|
|
||||||
{match.description && (
|
|
||||||
<div className="text-sm text-center text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
{match.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -41,7 +41,7 @@ export default function Switch({
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
{labelRight && (
|
{labelRight && (
|
||||||
<label htmlFor={id} className="text-sm text-gray-500 dark:text-neutral-400">
|
<label htmlFor={id} className="text-sm text-gray-500 dark:text-neutral-400 cursor-pointer">
|
||||||
{labelRight}
|
{labelRight}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export default function TeamCard({
|
|||||||
className="
|
className="
|
||||||
p-4 border rounded-lg bg-white dark:bg-neutral-800
|
p-4 border rounded-lg bg-white dark:bg-neutral-800
|
||||||
dark:border-neutral-700 shadow-sm hover:shadow-md
|
dark:border-neutral-700 shadow-sm hover:shadow-md
|
||||||
transition cursor-pointer focus:outline-none
|
transition cursor-pointer focus:outline-none hover:scale-105 hover:bg-neutral-200 hover:dark:bg-neutral-700
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{/* Kopfzeile */}
|
{/* Kopfzeile */}
|
||||||
@ -111,8 +111,9 @@ export default function TeamCard({
|
|||||||
{adminMode ? (
|
{adminMode ? (
|
||||||
<Button
|
<Button
|
||||||
title="Verwalten"
|
title="Verwalten"
|
||||||
size="sm"
|
size="md"
|
||||||
color="blue"
|
color="blue"
|
||||||
|
variant='solid'
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation() // ▼ Navigation hier unterbinden
|
e.stopPropagation() // ▼ Navigation hier unterbinden
|
||||||
router.push(`/admin/teams/${data.id}`)
|
router.push(`/admin/teams/${data.id}`)
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export default function TeamMemberView({
|
|||||||
const [logoPreview, setLogoPreview] = useState<string | null>(null)
|
const [logoPreview, setLogoPreview] = useState<string | null>(null)
|
||||||
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)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session?.user?.steamId) {
|
if (session?.user?.steamId) {
|
||||||
@ -205,6 +206,8 @@ export default function TeamMemberView({
|
|||||||
setactivePlayers(newActive)
|
setactivePlayers(newActive)
|
||||||
setInactivePlayers(newInactive)
|
setInactivePlayers(newInactive)
|
||||||
await updateTeamMembers(team!.id, newActive, newInactive)
|
await updateTeamMembers(team!.id, newActive, newInactive)
|
||||||
|
setSaveSuccess(true)
|
||||||
|
setTimeout(() => setSaveSuccess(false), 3000) // 3 Sekunden sichtbar
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmKick = async () => {
|
const confirmKick = async () => {
|
||||||
@ -488,13 +491,13 @@ 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}>
|
<DroppableZone id="active" label={`Aktives Team (${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}>
|
<DroppableZone id="inactive" label="Inaktives Team" 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 && (
|
||||||
|
|||||||
@ -1,144 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import Table from './Table';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { mapNameMap } from '../lib/mapNameMap';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import PremierRankBadge from './PremierRankBadge';
|
|
||||||
import CompRankBadge from './CompRankBadge';
|
|
||||||
|
|
||||||
interface Match {
|
|
||||||
id: string;
|
|
||||||
map: string;
|
|
||||||
date: string;
|
|
||||||
score: string;
|
|
||||||
winnerTeam?: string;
|
|
||||||
team?: 'CT' | 'T';
|
|
||||||
matchType: string;
|
|
||||||
rating: string;
|
|
||||||
kills: number;
|
|
||||||
deaths: number;
|
|
||||||
kdr: string;
|
|
||||||
rankNew: number;
|
|
||||||
rankOld: number;
|
|
||||||
rankChange: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UserMatchesTable({ steamId }: { steamId: string }) {
|
|
||||||
const [matches, setMatches] = useState<Match[]>([]);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!steamId) return
|
|
||||||
fetch(`/api/user/${steamId}/matches`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then(setMatches)
|
|
||||||
.catch(console.error);
|
|
||||||
}, [steamId])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
<Table.Head>
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell as="th">Map</Table.Cell>
|
|
||||||
<Table.Cell as="th">Date</Table.Cell>
|
|
||||||
<Table.Cell as="th">Score</Table.Cell>
|
|
||||||
<Table.Cell as="th">Rank</Table.Cell>
|
|
||||||
<Table.Cell as="th">Kills</Table.Cell>
|
|
||||||
<Table.Cell as="th">Deaths</Table.Cell>
|
|
||||||
<Table.Cell as="th">K/D</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Head>
|
|
||||||
<Table.Body>
|
|
||||||
{matches.map((m: Match) => {
|
|
||||||
const mapInfo = mapNameMap[m.map] ?? mapNameMap['lobby_mapveto'];
|
|
||||||
const [scoreCT, scoreT] = m.score.split(':').map(s => parseInt(s.trim(), 10));
|
|
||||||
|
|
||||||
let left = scoreCT;
|
|
||||||
let right = scoreT;
|
|
||||||
|
|
||||||
// Score-Reihenfolge anhand des eigenen Teams und Sieger drehen
|
|
||||||
if (m.team === 'T') {
|
|
||||||
left = scoreT;
|
|
||||||
right = scoreCT;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Score-Farbe bestimmen
|
|
||||||
let scoreClass = '';
|
|
||||||
if (!isNaN(left) && !isNaN(right)) {
|
|
||||||
if (left > right) {
|
|
||||||
scoreClass = 'bg-green-50 dark:bg-green-950';
|
|
||||||
} else if (left < right) {
|
|
||||||
scoreClass = 'bg-red-50 dark:bg-red-950';
|
|
||||||
} else {
|
|
||||||
scoreClass = 'bg-yellow-50 dark:bg-yellow-950';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table.Row
|
|
||||||
key={m.id}
|
|
||||||
hoverable
|
|
||||||
onClick={() => router.push(`/match-details/${m.id}`)}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
<Table.Cell>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<img
|
|
||||||
src={`/assets/img/mapicons/${m.map}.webp`}
|
|
||||||
alt={mapInfo.name}
|
|
||||||
height={32}
|
|
||||||
width={32}
|
|
||||||
/>
|
|
||||||
{mapInfo.name}
|
|
||||||
</div>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>{new Date(m.date).toLocaleString()}</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
m.winnerTeam === m.team
|
|
||||||
? 'text-green-600 dark:text-green-400'
|
|
||||||
: m.winnerTeam && m.winnerTeam !== 'Draw'
|
|
||||||
? 'text-red-600 dark:text-red-400'
|
|
||||||
: 'text-yellow-600 dark:text-yellow-400'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{`${left} : ${right}`}
|
|
||||||
</span>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="whitespace-nowrap">
|
|
||||||
<div className="flex items-center gap-[6px]">
|
|
||||||
{m.matchType === 'premier' ? (
|
|
||||||
<PremierRankBadge rank={m.rankNew} />
|
|
||||||
) : (
|
|
||||||
<CompRankBadge rank={m.rankNew} />
|
|
||||||
)}
|
|
||||||
{m.rankChange !== null && m.matchType === 'premier' && (
|
|
||||||
<span
|
|
||||||
className={`text-sm ${
|
|
||||||
m.rankChange > 0
|
|
||||||
? 'text-green-500'
|
|
||||||
: m.rankChange < 0
|
|
||||||
? 'text-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{m.rankChange > 0 ? '+' : ''}
|
|
||||||
{m.rankChange}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>{m.kills}</Table.Cell>
|
|
||||||
<Table.Cell>{m.deaths}</Table.Cell>
|
|
||||||
<Table.Cell>{m.kdr}</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -10,6 +10,8 @@ import DatePickerWithTime from '../DatePickerWithTime'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Switch from '../Switch'
|
import Switch from '../Switch'
|
||||||
|
import CommunityMatchList from '../CommunityMatchList'
|
||||||
|
import Card from '../Card'
|
||||||
|
|
||||||
function getRoundedDate() {
|
function getRoundedDate() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@ -27,16 +29,12 @@ function getTeamLogo(logo?: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MatchesAdminManager() {
|
export default function MatchesAdminManager() {
|
||||||
const { data: session } = useSession()
|
|
||||||
const [teams, setTeams] = useState<any[]>([])
|
const [teams, setTeams] = useState<any[]>([])
|
||||||
const [matches, setMatches] = useState<any[]>([])
|
const [matches, setMatches] = useState<any[]>([])
|
||||||
const [teamAId, setTeamAId] = useState('')
|
const [teamAId, setTeamAId] = useState('')
|
||||||
const [teamBId, setTeamBId] = useState('')
|
const [teamBId, setTeamBId] = useState('')
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [titleManuallySet, setTitleManuallySet] = useState(false)
|
const [titleManuallySet, setTitleManuallySet] = useState(false)
|
||||||
const [description, setDescription] = useState('')
|
|
||||||
const [matchDate, setMatchDate] = useState(getRoundedDate())
|
|
||||||
const [showModal, setShowModal] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/admin/teams').then(res => res.json()).then(setTeams)
|
fetch('/api/admin/teams').then(res => res.json()).then(setTeams)
|
||||||
@ -58,153 +56,9 @@ export default function MatchesAdminManager() {
|
|||||||
if (res.ok) setMatches(await res.json())
|
if (res.ok) setMatches(await res.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredMatches = matches.filter(
|
|
||||||
(m: any) => m.matchType === 'community'
|
|
||||||
)
|
|
||||||
|
|
||||||
const resetFields = () => {
|
|
||||||
setTitle('')
|
|
||||||
setTitleManuallySet(false)
|
|
||||||
setDescription('')
|
|
||||||
setMatchDate(getRoundedDate())
|
|
||||||
setTeamAId('')
|
|
||||||
setTeamBId('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const createMatch = async () => {
|
|
||||||
if (!teamAId || !teamBId || !title || !matchDate || teamAId === teamBId) {
|
|
||||||
alert('Bitte alle Felder korrekt ausfüllen.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch('/api/matches/create', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ teamAId, teamBId, title, description, matchDate })
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
resetFields()
|
|
||||||
setShowModal(false)
|
|
||||||
fetchMatches()
|
|
||||||
} else {
|
|
||||||
alert('Fehler beim Erstellen')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Card maxWidth='auto'>
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4 space-y-4">
|
<CommunityMatchList matchType="community" />
|
||||||
<div className="max-w-4xl mx-auto px-4 flex justify-between items-center mb-4">
|
</Card>
|
||||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">Geplante Matches</h1>
|
|
||||||
<Button onClick={() => setShowModal(true)} color="blue">
|
|
||||||
Neues Match erstellen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{filteredMatches.length === 0 ? (
|
|
||||||
<p className="text-gray-500">Keine Matches geplant.</p>
|
|
||||||
) : (
|
|
||||||
<ul className="space-y-4">
|
|
||||||
{filteredMatches.map((match: any) => (
|
|
||||||
<li key={match.id}>
|
|
||||||
<Link href={`/matches/${match.id}`} className="block border rounded p-4 hover:bg-gray-50 dark:hover:bg-neutral-800 transition">
|
|
||||||
<div className="flex items-center justify-between text-center">
|
|
||||||
<div className="flex flex-col items-center w-1/4">
|
|
||||||
<Image
|
|
||||||
src={getTeamLogo(match.teamA?.logo)}
|
|
||||||
alt={match.teamA?.name || 'Team A'}
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="rounded-full border object-cover bg-white"
|
|
||||||
/>
|
|
||||||
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
|
|
||||||
{match.teamA?.name || 'Team A'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300 w-1/2">
|
|
||||||
<div>{new Date(match.matchDate).toLocaleDateString('de-DE')}</div>
|
|
||||||
<div>{new Date(match.matchDate).toLocaleTimeString('de-DE', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})} Uhr</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center w-1/4">
|
|
||||||
<Image
|
|
||||||
src={getTeamLogo(match.teamB?.logo)}
|
|
||||||
alt={match.teamB?.name || 'Team B'}
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="rounded-full border object-cover bg-white"
|
|
||||||
/>
|
|
||||||
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
|
|
||||||
{match.teamB?.name || 'Team B'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{match.description && (
|
|
||||||
<div className="text-sm text-center text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
{match.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modal zum Erstellen */}
|
|
||||||
<Modal
|
|
||||||
id="create-match-modal"
|
|
||||||
title="Match erstellen"
|
|
||||||
show={showModal}
|
|
||||||
onClose={() => setShowModal(false)}
|
|
||||||
onSave={createMatch}
|
|
||||||
closeButtonTitle="Erstellen"
|
|
||||||
closeButtonColor="blue"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-2 dark:text-white">Team A</label>
|
|
||||||
<Select
|
|
||||||
value={teamAId}
|
|
||||||
onChange={setTeamAId}
|
|
||||||
options={teams.filter(t => t.id !== teamBId).map(t => ({ value: t.id, label: t.name }))}
|
|
||||||
placeholder="Wählen"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-2 dark:text-white">Team B</label>
|
|
||||||
<Select
|
|
||||||
value={teamBId}
|
|
||||||
onChange={setTeamBId}
|
|
||||||
options={teams.filter(t => t.id !== teamAId).map(t => ({ value: t.id, label: t.name }))}
|
|
||||||
placeholder="Wählen"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Input
|
|
||||||
label="Titel"
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => {
|
|
||||||
setTitle(e.target.value)
|
|
||||||
setTitleManuallySet(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Input
|
|
||||||
label="Beschreibung"
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<DatePickerWithTime value={matchDate} onChange={setMatchDate} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import Modal from '@/app/components/Modal'
|
|||||||
import Input from '@/app/components/Input'
|
import Input from '@/app/components/Input'
|
||||||
import TeamCard from '@/app/components/TeamCard'
|
import TeamCard from '@/app/components/TeamCard'
|
||||||
import type { Team } from '@/app/types/team'
|
import type { Team } from '@/app/types/team'
|
||||||
|
import LoadingSpinner from '../../LoadingSpinner'
|
||||||
|
|
||||||
export default function AdminTeamsView() {
|
export default function AdminTeamsView() {
|
||||||
/* ────────────────────────── Session ─────────────────────────── */
|
/* ────────────────────────── Session ─────────────────────────── */
|
||||||
@ -75,7 +76,7 @@ export default function AdminTeamsView() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
Lade Teams …
|
<LoadingSpinner />
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
137
src/app/components/profile/[steamId]/matches/UserMatchesList.tsx
Normal file
137
src/app/components/profile/[steamId]/matches/UserMatchesList.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
import Table from '../../../Table'
|
||||||
|
import PremierRankBadge from '../../../PremierRankBadge'
|
||||||
|
import CompRankBadge from '../../../CompRankBadge'
|
||||||
|
import { mapNameMap } from '@/app/lib/mapNameMap'
|
||||||
|
|
||||||
|
/* ───────── Typen ───────── */
|
||||||
|
interface Match {
|
||||||
|
id : string
|
||||||
|
map : string
|
||||||
|
date : string
|
||||||
|
score : string | null
|
||||||
|
winnerTeam?: 'CT' | 'T' | 'Draw'
|
||||||
|
team? : 'CT' | 'T'
|
||||||
|
matchType : 'premier' | 'competitive' | string
|
||||||
|
rating : string
|
||||||
|
kills : number
|
||||||
|
deaths : number
|
||||||
|
kdr : string
|
||||||
|
rankNew : number
|
||||||
|
rankOld : number
|
||||||
|
rankChange : number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── Hilfsfunktionen ───────── */
|
||||||
|
const parseScore = (raw?: string | null): [number, number] => {
|
||||||
|
if (!raw) return [0, 0]
|
||||||
|
const [a, b] = raw.split(':').map(n => Number(n.trim()))
|
||||||
|
return [Number.isNaN(a) ? 0 : a, Number.isNaN(b) ? 0 : b]
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────── Komponente ───────── */
|
||||||
|
export default function UserMatchesList({ steamId }: { steamId: string }) {
|
||||||
|
const [matches, setMatches] = useState<Match[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!steamId) return
|
||||||
|
fetch(`/api/user/${steamId}/matches?types=premier,competitive`)
|
||||||
|
.then(r => r.ok ? r.json() : [])
|
||||||
|
.then(setMatches)
|
||||||
|
.catch(console.error)
|
||||||
|
}, [steamId])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
{/* Kopf */}
|
||||||
|
<Table.Head>
|
||||||
|
<Table.Row>
|
||||||
|
{['Map','Date','Score','Rank','Kills','Deaths','K/D'].map(h => (
|
||||||
|
<Table.Cell key={h} as="th">{h}</Table.Cell>
|
||||||
|
))}
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Head>
|
||||||
|
|
||||||
|
{/* Daten */}
|
||||||
|
<Table.Body>
|
||||||
|
{matches.map(m => {
|
||||||
|
const mapInfo = mapNameMap[m.map] ?? mapNameMap.lobby_mapveto
|
||||||
|
const [scoreCT, scoreT] = parseScore(m.score)
|
||||||
|
|
||||||
|
/* Score aus Sicht des Spielers drehen */
|
||||||
|
const ownCTSide = m.team !== 'T'
|
||||||
|
const left = ownCTSide ? scoreCT : scoreT
|
||||||
|
const right = ownCTSide ? scoreT : scoreCT
|
||||||
|
|
||||||
|
/* Text-Farbe für Score */
|
||||||
|
const scoreColor =
|
||||||
|
left > right ? 'text-green-600 dark:text-green-400'
|
||||||
|
: left < right ? 'text-red-600 dark:text-red-400'
|
||||||
|
: 'text-yellow-600 dark:text-yellow-400'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Row
|
||||||
|
key={m.id}
|
||||||
|
hoverable
|
||||||
|
onClick={() => router.push(`/match-details/${m.id}`)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
{/* Map + Icon */}
|
||||||
|
<Table.Cell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
src={`/assets/img/mapicons/${m.map}.webp`}
|
||||||
|
alt={mapInfo.name}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
{mapInfo.name}
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
|
||||||
|
{/* Datum */}
|
||||||
|
<Table.Cell>{new Date(m.date).toLocaleString()}</Table.Cell>
|
||||||
|
|
||||||
|
{/* Score */}
|
||||||
|
<Table.Cell>
|
||||||
|
<span className={`font-medium ${scoreColor}`}>
|
||||||
|
{left} : {right}
|
||||||
|
</span>
|
||||||
|
</Table.Cell>
|
||||||
|
|
||||||
|
{/* Rank + Delta */}
|
||||||
|
<Table.Cell className="whitespace-nowrap">
|
||||||
|
<div className="flex items-center gap-[6px]">
|
||||||
|
{m.matchType === 'premier'
|
||||||
|
? <PremierRankBadge rank={m.rankNew} />
|
||||||
|
: <CompRankBadge rank={m.rankNew} />}
|
||||||
|
{m.rankChange !== null && m.matchType === 'premier' && (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
m.rankChange > 0 ? 'text-green-500'
|
||||||
|
: m.rankChange < 0 ? 'text-red-500'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{m.rankChange > 0 ? '+' : ''}{m.rankChange}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<Table.Cell>{m.kills}</Table.Cell>
|
||||||
|
<Table.Cell>{m.deaths}</Table.Cell>
|
||||||
|
<Table.Cell>{m.kdr}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -3,8 +3,8 @@
|
|||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import Chart from '@/app/components/Chart'
|
import Chart from '@/app/components/Chart'
|
||||||
import { MatchStats } from '@/app/types/match'
|
import { MatchStats } from '@/app/types/match'
|
||||||
import Card from './Card'
|
import Card from '../../../Card'
|
||||||
import UserClips from './UserClips'
|
import UserClips from '../../../UserClips'
|
||||||
|
|
||||||
type MatchStatsProps = {
|
type MatchStatsProps = {
|
||||||
stats: { matches: MatchStats[] }
|
stats: { matches: MatchStats[] }
|
||||||
38
src/app/lib/removePlayerFromMatches.ts
Normal file
38
src/app/lib/removePlayerFromMatches.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { prisma } from '@/app/lib/prisma'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt einen Spieler aus allen Matches, in denen er noch in teamAUsers
|
||||||
|
* oder teamBUsers steht – und löscht zugleich den MatchPlayer-Eintrag.
|
||||||
|
*
|
||||||
|
* @param teamId – Team, das bearbeitet wird
|
||||||
|
* @param steamId – Spieler-SteamID
|
||||||
|
*/
|
||||||
|
export async function removePlayerFromMatches(teamId: string, steamId: string) {
|
||||||
|
// alle betroffenen Matches holen
|
||||||
|
const matches = await prisma.match.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ teamAId: teamId, teamAUsers: { some: { steamId } } },
|
||||||
|
{ teamBId: teamId, teamBUsers: { some: { steamId } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.$transaction(
|
||||||
|
matches.flatMap(m => [
|
||||||
|
// Relation aus teamAUsers / teamBUsers lösen
|
||||||
|
prisma.match.update({
|
||||||
|
where: { id: m.id },
|
||||||
|
data : {
|
||||||
|
teamAUsers: { disconnect: { steamId } },
|
||||||
|
teamBUsers: { disconnect: { steamId } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// ggf. vorhandenen MatchPlayer-Satz löschen
|
||||||
|
prisma.matchPlayer.deleteMany({
|
||||||
|
where: { matchId: m.id, steamId },
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// /app/profile/[steamId]/matches/page.tsx
|
// /app/profile/[steamId]/matches/page.tsx
|
||||||
import UserMatchesTable from '@/app/components/UserMatchesTable'
|
import UserMatchesList from '@/app/components/profile/[steamId]/matches/UserMatchesList'
|
||||||
import Card from '@/app/components/Card'
|
import Card from '@/app/components/Card'
|
||||||
|
|
||||||
export default function MatchesPage({ params }: { params: { steamId: string } }) {
|
export default function MatchesPage({ params }: { params: { steamId: string } }) {
|
||||||
@ -10,7 +10,7 @@ export default function MatchesPage({ params }: { params: { steamId: string } })
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Card maxWidth="auto">
|
<Card maxWidth="auto">
|
||||||
<UserMatchesTable steamId={params.steamId} />
|
<UserMatchesList steamId={params.steamId} />
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
// /app/profile/[steamId]/matches/page.tsx
|
// /app/profile/[steamId]/matches/page.tsx
|
||||||
import Card from '@/app/components/Card'
|
import Card from '@/app/components/Card'
|
||||||
import UserMatchesTable from '@/app/components/UserMatchesTable'
|
import UserMatchesList from '@/app/components/profile/[steamId]/matches/UserMatchesList'
|
||||||
|
|
||||||
export default function MatchesPage({ params }: { params: { steamId: string } }) {
|
export default function MatchesPage({ params }: { params: { steamId: string } }) {
|
||||||
return (
|
return (
|
||||||
<Card maxWidth="auto">
|
<Card maxWidth="auto">
|
||||||
<UserMatchesTable steamId={params.steamId} />
|
<UserMatchesList steamId={params.steamId} />
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// /app/profile/[steamId]/stats/page.tsx
|
// /app/profile/[steamId]/stats/page.tsx
|
||||||
import UserProfile from '@/app/components/UserProfile'
|
import UserProfile from '@/app/components/profile/[steamId]/stats/UserProfile'
|
||||||
import { MatchStats } from '@/app/types/match'
|
import { MatchStats } from '@/app/types/match'
|
||||||
|
|
||||||
async function getStats(steamId: string) {
|
async function getStats(steamId: string) {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import Switch from '@/app/components/Switch'
|
import Switch from '@/app/components/Switch'
|
||||||
import Button from '../components/Button'
|
import Button from '../components/Button'
|
||||||
|
import CommunityMatchList from '../components/CommunityMatchList'
|
||||||
|
import Card from '../components/Card'
|
||||||
|
|
||||||
type Match = {
|
type Match = {
|
||||||
id: string
|
id: string
|
||||||
@ -15,10 +17,6 @@ type Match = {
|
|||||||
teamB: { id: string; name: string; logo?: string | null }
|
teamB: { id: string; name: string; logo?: string | null }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTeamLogo(logo?: string | null) {
|
|
||||||
return logo ? `/assets/img/logos/${logo}` : '/assets/img/logos/cs2.webp'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MatchesPage() {
|
export default function MatchesPage() {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const [matches, setMatches] = useState<Match[]>([])
|
const [matches, setMatches] = useState<Match[]>([])
|
||||||
@ -41,83 +39,8 @@ export default function MatchesPage() {
|
|||||||
: matches
|
: matches
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4 space-y-4">
|
<Card maxWidth='auto'>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<CommunityMatchList matchType="community" />
|
||||||
<h1 className="text-2xl font-bold text-gray-700 dark:text-neutral-300">Geplante Matches</h1>
|
</Card>
|
||||||
|
|
||||||
{session?.user?.team && (
|
|
||||||
<Switch
|
|
||||||
id="only-own-team"
|
|
||||||
checked={onlyOwnTeam}
|
|
||||||
onChange={setOnlyOwnTeam}
|
|
||||||
labelRight="Nur mein Team"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
{session?.user?.isAdmin && (
|
|
||||||
<Link href="/admin/matches">
|
|
||||||
<Button color="blue">Match erstellen</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filteredMatches.length === 0 ? (
|
|
||||||
<p className="text-gray-700 dark:text-neutral-300">Keine Matches geplant.</p>
|
|
||||||
) : (
|
|
||||||
<ul className="space-y-4">
|
|
||||||
{filteredMatches.map(match => (
|
|
||||||
<li key={match.id}>
|
|
||||||
<Link
|
|
||||||
href={`/match-details/${match.id}`}
|
|
||||||
className="block border rounded p-4 hover:bg-gray-50 dark:hover:bg-neutral-800 transition"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between text-center">
|
|
||||||
{/* Team A */}
|
|
||||||
<div className="flex flex-col items-center w-1/4">
|
|
||||||
<Image
|
|
||||||
src={getTeamLogo(match.teamA?.logo)}
|
|
||||||
alt={match.teamA?.name || 'Team A'}
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="rounded-full border object-cover bg-white"
|
|
||||||
/>
|
|
||||||
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
|
|
||||||
{match.teamA?.name ?? 'Team A'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Datum / Zeit */}
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300 w-1/2">
|
|
||||||
<div>{new Date(match.matchDate).toLocaleDateString('de-DE')}</div>
|
|
||||||
<div>
|
|
||||||
{new Date(match.matchDate).toLocaleTimeString('de-DE', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}{' '}
|
|
||||||
Uhr
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Team B */}
|
|
||||||
<div className="flex flex-col items-center w-1/4">
|
|
||||||
<Image
|
|
||||||
src={getTeamLogo(match.teamB?.logo)}
|
|
||||||
alt={match.teamB?.name || 'Team B'}
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="rounded-full border object-cover bg-white"
|
|
||||||
/>
|
|
||||||
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
|
|
||||||
{match.teamB?.name ?? 'Team B'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,24 @@
|
|||||||
|
// src/app/types/match.ts
|
||||||
|
|
||||||
import { Player } from './team'
|
import { Player } from './team'
|
||||||
|
|
||||||
export type Match = {
|
export type Match = {
|
||||||
|
/* Basis-Infos ---------------------------------------------------- */
|
||||||
id : string
|
id : string
|
||||||
title : string
|
title : string
|
||||||
demoDate: Date
|
demoDate : string // ⇐ Backend kommt als ISO-String
|
||||||
description?: string
|
description?: string
|
||||||
map : string
|
map : string
|
||||||
matchType: string
|
matchType : 'premier' | 'competitive' | 'community' | string
|
||||||
roundCount : number
|
roundCount : number
|
||||||
|
|
||||||
|
/* Ergebnis ------------------------------------------------------- */
|
||||||
scoreA? : number | null
|
scoreA? : number | null
|
||||||
scoreB? : number | null
|
scoreB? : number | null
|
||||||
|
/** CT | T | Draw | null – null, solange noch kein Ergebnis vorliegt */
|
||||||
|
winnerTeam? : 'CT' | 'T' | 'Draw' | null
|
||||||
|
|
||||||
|
/* Teams ---------------------------------------------------------- */
|
||||||
teamA: {
|
teamA: {
|
||||||
id : string
|
id : string
|
||||||
name : string
|
name : string
|
||||||
@ -26,6 +35,7 @@ export type Match = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------- */
|
||||||
export type MatchPlayer = {
|
export type MatchPlayer = {
|
||||||
user : Player
|
user : Player
|
||||||
stats?: {
|
stats?: {
|
||||||
@ -43,7 +53,7 @@ export type MatchPlayer = {
|
|||||||
smokeKills : number
|
smokeKills : number
|
||||||
headshots : number
|
headshots : number
|
||||||
noScopes : number
|
noScopes : number
|
||||||
blindKills: Number
|
blindKills : number
|
||||||
rankOld : number
|
rankOld : number
|
||||||
rankNew : number
|
rankNew : number
|
||||||
rankChange : number
|
rankChange : number
|
||||||
@ -55,16 +65,16 @@ export type MatchPlayer = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------- */
|
||||||
export type MatchStats = {
|
export type MatchStats = {
|
||||||
date: string
|
date : string // ISO
|
||||||
kills : number
|
kills : number
|
||||||
deaths : number
|
deaths : number
|
||||||
assists : number
|
assists : number
|
||||||
headshotPct: number,
|
headshotPct : number
|
||||||
totalDamage: number,
|
totalDamage : number
|
||||||
map: string,
|
map : string
|
||||||
matchType: 'premier' | 'competitive' | 'community' // falls du auch andere hast
|
matchType : 'premier' | 'competitive' | 'community'
|
||||||
rankNew : number | null
|
rankNew : number | null
|
||||||
rankOld? : number | null
|
rankOld? : number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,4 +15,5 @@ export type Team = {
|
|||||||
leader?: string | null
|
leader?: string | null
|
||||||
activePlayers? : Player[]
|
activePlayers? : Player[]
|
||||||
inactivePlayers?: Player[]
|
inactivePlayers?: Player[]
|
||||||
|
invitedPlayers?: Player[]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user