update
This commit is contained in:
parent
90a3bdeb35
commit
ad4fe7c29a
@ -17,7 +17,7 @@ export default function AdminPage() {
|
||||
switch (activeTab) {
|
||||
case 'matches':
|
||||
return (
|
||||
<Card title="Matches">
|
||||
<Card title="Matches" maxWidth='auto'>
|
||||
<MatchesAdminManager />
|
||||
</Card>
|
||||
)
|
||||
|
||||
@ -1,53 +1,53 @@
|
||||
// /app/api/matches/route.ts
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(req: Request) {
|
||||
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({
|
||||
where : matchType ? { matchType } : undefined,
|
||||
orderBy: { demoDate: 'desc' },
|
||||
include: {
|
||||
teamA: true,
|
||||
teamB: true,
|
||||
players: {
|
||||
include: {
|
||||
user: true,
|
||||
stats: true,
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
teamA : true,
|
||||
teamB : true,
|
||||
players: { include: { user: true, stats: true, team: true } },
|
||||
},
|
||||
})
|
||||
|
||||
const formatted = matches.map(match => ({
|
||||
id: match.id,
|
||||
map: match.map,
|
||||
demoDate: match.demoDate,
|
||||
matchType: match.matchType,
|
||||
scoreA: match.scoreA,
|
||||
scoreB: match.scoreB,
|
||||
winnerTeam: match.winnerTeam ?? null,
|
||||
/* … rest bleibt unverändert … */
|
||||
const formatted = matches.map(m => ({
|
||||
id : m.id,
|
||||
map : m.map,
|
||||
demoDate: m.demoDate,
|
||||
matchType: m.matchType,
|
||||
scoreA : m.scoreA,
|
||||
scoreB : m.scoreB,
|
||||
winnerTeam: m.winnerTeam ?? null,
|
||||
teamA: {
|
||||
id: match.teamA?.id ?? null,
|
||||
name: match.teamA?.name ?? 'CT',
|
||||
logo: match.teamA?.logo ?? null,
|
||||
score: match.scoreA,
|
||||
id : m.teamA?.id ?? null,
|
||||
name: m.teamA?.name ?? 'CT',
|
||||
logo: m.teamA?.logo ?? null,
|
||||
score: m.scoreA,
|
||||
},
|
||||
teamB: {
|
||||
id: match.teamB?.id ?? null,
|
||||
name: match.teamB?.name ?? 'T',
|
||||
logo: match.teamB?.logo ?? null,
|
||||
score: match.scoreB,
|
||||
id : m.teamB?.id ?? null,
|
||||
name: m.teamB?.name ?? 'T',
|
||||
logo: m.teamB?.logo ?? null,
|
||||
score: m.scoreB,
|
||||
},
|
||||
players: match.players.map(p => ({
|
||||
steamId: p.steamId,
|
||||
name: p.user?.name,
|
||||
avatar: p.user?.avatar,
|
||||
stats: p.stats,
|
||||
teamId: p.teamId,
|
||||
players: m.players.map(p => ({
|
||||
steamId : p.steamId,
|
||||
name : p.user?.name,
|
||||
avatar : p.user?.avatar,
|
||||
stats : p.stats,
|
||||
teamId : p.teamId,
|
||||
teamName: p.team?.name ?? null,
|
||||
})),
|
||||
}));
|
||||
}))
|
||||
|
||||
return NextResponse.json(formatted)
|
||||
} catch (err) {
|
||||
|
||||
@ -1,40 +1,30 @@
|
||||
// src/app/api/team/kick/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
import { removePlayerFromMatches } from '@/app/lib/removePlayerFromMatches'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 1) Payload-Validierung *
|
||||
* ------------------------------------------------------------------ */
|
||||
/* ───────── 1) Payload prüfen ───────── */
|
||||
const { teamId, steamId } = await req.json()
|
||||
|
||||
if (!teamId || !steamId) {
|
||||
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 } })
|
||||
if (!team) {
|
||||
return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
if (!team) return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { steamId },
|
||||
where : { steamId },
|
||||
select: { name: true },
|
||||
})
|
||||
|
||||
const userName = user?.name ?? 'Ein Mitglied'
|
||||
const teamName = team.name ?? 'Unbekanntes Team'
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 3) Spielerlisten aktualisieren *
|
||||
* ------------------------------------------------------------------ */
|
||||
/* ───────── 3) Spieler aus Team-Arrays entfernen ───────── */
|
||||
const active = team.activePlayers.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 */
|
||||
await prisma.user.update({
|
||||
where: { steamId },
|
||||
data : { teamId: null },
|
||||
})
|
||||
/* ───────── 4) User vom Team lösen ───────── */
|
||||
await prisma.user.update({ where: { steamId }, data: { teamId: null } })
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 4) Notifikation für den gekickten User *
|
||||
* ------------------------------------------------------------------ */
|
||||
const kickedNotification = await prisma.notification.create({
|
||||
/* ───────── 5) Spieler aus offenen Matches werfen ───────── */
|
||||
await removePlayerFromMatches(teamId, steamId)
|
||||
|
||||
/* ───────── 6) Notifications & SSE ───────── */
|
||||
|
||||
/* an gekickten User */
|
||||
const kickedN = await prisma.notification.create({
|
||||
data: {
|
||||
user : { connect: { steamId } },
|
||||
title : 'Team verlassen',
|
||||
message : `Du wurdest aus dem Team „${teamName}“ geworfen.`,
|
||||
actionType : 'team-kick',
|
||||
actionData : null,
|
||||
user : { connect: { steamId } }, // <-- Relation herstellen
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type : kickedNotification.actionType ?? 'notification',
|
||||
type : kickedN.actionType ?? 'notification',
|
||||
targetUserIds: [steamId],
|
||||
message : kickedNotification.message,
|
||||
id : kickedNotification.id,
|
||||
actionType : kickedNotification.actionType ?? undefined,
|
||||
actionData : kickedNotification.actionData ?? undefined,
|
||||
createdAt : kickedNotification.createdAt.toISOString(),
|
||||
id : kickedN.id,
|
||||
message : kickedN.message,
|
||||
createdAt : kickedN.createdAt.toISOString(),
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 5) Notifikation für verbleibende Mitglieder *
|
||||
* ------------------------------------------------------------------ */
|
||||
const remainingUserIds = [...active, ...inactive]
|
||||
|
||||
/* an verbleibende Mitglieder */
|
||||
const remaining = [...active, ...inactive]
|
||||
await Promise.all(
|
||||
remainingUserIds.map(async memberSteamId => {
|
||||
remaining.map(async uid => {
|
||||
const n = await prisma.notification.create({
|
||||
data: {
|
||||
user : { connect: { steamId: uid } },
|
||||
title : 'Team-Update',
|
||||
message : `${userName} wurde aus dem Team „${teamName}“ geworfen.`,
|
||||
message : `${userName} wurde aus dem Team „${teamName}“ gekickt.`,
|
||||
actionType : 'team-kick-other',
|
||||
actionData : null,
|
||||
user : { connect: { steamId: memberSteamId } }, // <-- Relation
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type : n.actionType ?? 'notification',
|
||||
targetUserIds: [memberSteamId],
|
||||
message : n.message,
|
||||
id : n.id,
|
||||
actionType : n.actionType ?? undefined,
|
||||
actionData : n.actionData ?? undefined,
|
||||
createdAt : n.createdAt.toISOString(),
|
||||
type : n.actionType ?? 'notification',
|
||||
targetUserIds: [uid],
|
||||
id : n.id,
|
||||
message : n.message,
|
||||
createdAt : n.createdAt.toISOString(),
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 6) Erfolg *
|
||||
* ------------------------------------------------------------------ */
|
||||
/* ► UI neu laden lassen */
|
||||
await sendServerSSEMessage({
|
||||
type : 'team-updated',
|
||||
teamId,
|
||||
targetUserIds : remaining,
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Mitglied entfernt' })
|
||||
} catch (error) {
|
||||
console.error('[KICK] Fehler:', error)
|
||||
} catch (err) {
|
||||
console.error('[KICK] Fehler:', err)
|
||||
return NextResponse.json({ message: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,123 +1,110 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { removePlayerFromTeam } from '@/app/lib/removePlayerFromTeam'
|
||||
import { removePlayerFromMatches } from '@/app/lib/removePlayerFromMatches'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { steamId } = await req.json()
|
||||
|
||||
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({
|
||||
where: {
|
||||
OR: [
|
||||
{ activePlayers: { has: steamId } },
|
||||
{ activePlayers : { has: steamId } },
|
||||
{ inactivePlayers: { has: steamId } },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
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(
|
||||
{
|
||||
activePlayers: team.activePlayers,
|
||||
inactivePlayers: team.inactivePlayers,
|
||||
leader: team.leaderId,
|
||||
}, steamId)
|
||||
{ activePlayers: team.activePlayers, inactivePlayers: team.inactivePlayers, leader: team.leaderId },
|
||||
steamId,
|
||||
)
|
||||
|
||||
/* ───────── 2) Team anpassen / löschen ───────── */
|
||||
if (!leader) {
|
||||
await prisma.team.delete({ where: { id: team.id } })
|
||||
} else {
|
||||
await prisma.team.update({
|
||||
where: { id: team.id },
|
||||
data: {
|
||||
leader: {
|
||||
connect: { steamId: leader },
|
||||
},
|
||||
data : {
|
||||
leader: { connect: { steamId: leader } },
|
||||
activePlayers,
|
||||
inactivePlayers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { steamId },
|
||||
data: { teamId: null },
|
||||
})
|
||||
/* ───────── 3) User lösen ───────── */
|
||||
await prisma.user.update({ where: { steamId }, data: { teamId: null } })
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { steamId },
|
||||
select: { name: true },
|
||||
})
|
||||
/* ───────── 4) Spieler aus Matches entfernen ───────── */
|
||||
await removePlayerFromMatches(team.id, steamId)
|
||||
|
||||
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: {
|
||||
user: {
|
||||
connect: { steamId },
|
||||
},
|
||||
title: 'Teamupdate',
|
||||
message: `Du hast das Team "${team.name}" verlassen.`,
|
||||
actionType: 'team-left',
|
||||
actionData: null,
|
||||
user : { connect: { steamId } },
|
||||
title : 'Teamupdate',
|
||||
message : `Du hast das Team „${teamName}“ verlassen.`,
|
||||
actionType : 'team-left',
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
type : leaveN.actionType ?? 'notification',
|
||||
targetUserIds: [steamId],
|
||||
message: notification.message,
|
||||
id: notification.id,
|
||||
actionType: notification.actionType ?? undefined,
|
||||
actionData: notification.actionData ?? undefined,
|
||||
createdAt: notification.createdAt.toISOString(),
|
||||
id : leaveN.id,
|
||||
message : leaveN.message,
|
||||
createdAt : leaveN.createdAt.toISOString(),
|
||||
})
|
||||
|
||||
const allRemainingPlayers = Array.from(new Set([
|
||||
...activePlayers,
|
||||
...inactivePlayers,
|
||||
])).filter(id => id !== steamId)
|
||||
|
||||
/* an verbleibende Mitglieder */
|
||||
await Promise.all(
|
||||
allRemainingPlayers.map(async (userId) => {
|
||||
const notification = await prisma.notification.create({
|
||||
remaining.map(async uid => {
|
||||
const n = await prisma.notification.create({
|
||||
data: {
|
||||
user: {
|
||||
connect: { steamId: userId },
|
||||
},
|
||||
title: 'Teamupdate',
|
||||
message: `${user?.name ?? 'Ein Spieler'} hat das Team verlassen.`,
|
||||
actionType: 'team-member-left',
|
||||
actionData: null,
|
||||
user : { connect: { steamId: uid } },
|
||||
title : 'Teamupdate',
|
||||
message : `${userName} hat das Team verlassen.`,
|
||||
actionType : 'team-member-left',
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
targetUserIds: [userId],
|
||||
message: notification.message,
|
||||
id: notification.id,
|
||||
actionType: notification.actionType ?? undefined,
|
||||
actionData: notification.actionData ?? undefined,
|
||||
createdAt: notification.createdAt.toISOString(),
|
||||
type : n.actionType ?? 'notification',
|
||||
targetUserIds: [uid],
|
||||
id : n.id,
|
||||
message : n.message,
|
||||
createdAt : n.createdAt.toISOString(),
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: 'team-updated',
|
||||
teamId: team.id,
|
||||
targetUserIds: allRemainingPlayers,
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
/* ► UI neu laden lassen */
|
||||
if (remaining.length) {
|
||||
await sendServerSSEMessage({
|
||||
type : 'team-updated',
|
||||
teamId : team.id,
|
||||
targetUserIds : remaining,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Erfolgreich aus dem Team entfernt' })
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Verlassen des Teams:', error)
|
||||
return NextResponse.json({ message: 'Fehler beim Verlassen des Teams' }, { status: 500 })
|
||||
} catch (err) {
|
||||
console.error('[LEAVE] Fehler:', err)
|
||||
return NextResponse.json({ message: 'Serverfehler' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,91 +1,107 @@
|
||||
// /app/api/user/[steamId]/matches/route.ts
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: { steamId: string } }
|
||||
req : NextRequest, // ← Request wird gebraucht!
|
||||
{ params }: { params: { steamId: string } },
|
||||
) {
|
||||
const steamId = params.steamId
|
||||
|
||||
if (!steamId) {
|
||||
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 {
|
||||
const matchPlayers = await prisma.matchPlayer.findMany({
|
||||
where: { steamId },
|
||||
where: {
|
||||
steamId,
|
||||
|
||||
/* nur wenn Filter gesetzt ist */
|
||||
...(types.length && {
|
||||
match: { matchType: { in: types } },
|
||||
}),
|
||||
},
|
||||
|
||||
select: {
|
||||
teamId: true,
|
||||
team: true,
|
||||
match: {
|
||||
team : true,
|
||||
match : {
|
||||
select: {
|
||||
id: true,
|
||||
demoDate: true,
|
||||
map: true,
|
||||
roundCount: true,
|
||||
scoreA: true,
|
||||
scoreB: true,
|
||||
matchType: true,
|
||||
teamAId: true,
|
||||
teamBId: true,
|
||||
teamAUsers: true,
|
||||
teamBUsers: true,
|
||||
winnerTeam: true,
|
||||
id : true,
|
||||
demoDate : true,
|
||||
map : true,
|
||||
roundCount : true,
|
||||
scoreA : true,
|
||||
scoreB : true,
|
||||
matchType : true,
|
||||
teamAId : true,
|
||||
teamBId : true,
|
||||
teamAUsers : { select: { steamId: true } },
|
||||
teamBUsers : { select: { steamId: true } },
|
||||
winnerTeam : true,
|
||||
},
|
||||
},
|
||||
stats: true,
|
||||
},
|
||||
orderBy: {
|
||||
match: {
|
||||
demoDate: 'desc',
|
||||
},
|
||||
},
|
||||
|
||||
orderBy: { match: { demoDate: 'desc' } },
|
||||
})
|
||||
|
||||
const data = matchPlayers.map((mp) => {
|
||||
const match = mp.match
|
||||
/* ───────── Aufbereiten fürs Frontend ───────── */
|
||||
const data = matchPlayers.map(mp => {
|
||||
const m = mp.match
|
||||
const stats = mp.stats
|
||||
const kills = stats?.kills ?? 0
|
||||
|
||||
const kills = stats?.kills ?? 0
|
||||
const deaths = stats?.deaths ?? 0
|
||||
const kdr = deaths > 0 ? (kills / deaths).toFixed(2) : '∞'
|
||||
const roundCount = match.roundCount
|
||||
const kdr = deaths ? (kills / deaths).toFixed(2) : '∞'
|
||||
|
||||
const rankOld = stats?.rankOld ?? null
|
||||
const rankNew = stats?.rankNew ?? null
|
||||
|
||||
const rankChange =
|
||||
typeof rankNew === 'number' && typeof rankOld === 'number'
|
||||
? rankNew - rankOld
|
||||
: null
|
||||
const matchType = match.matchType ?? 'community'
|
||||
rankNew != null && rankOld != null ? rankNew - rankOld : null
|
||||
|
||||
const isInTeamA = match.teamAUsers.some((user) => user.steamId === steamId)
|
||||
const playerTeam = isInTeamA ? 'CT' : 'T'
|
||||
/* Team des Spielers ermitteln */
|
||||
const playerTeam =
|
||||
m.teamAUsers.some(u => u.steamId === steamId) ? 'CT' : 'T'
|
||||
|
||||
const scoreCT = match.scoreA ?? 0
|
||||
const scoreT = match.scoreB ?? 0
|
||||
const score = `${scoreCT} : ${scoreT}`
|
||||
const score = `${m.scoreA ?? 0} : ${m.scoreB ?? 0}`
|
||||
|
||||
return {
|
||||
id: match.id,
|
||||
map: match.map ?? 'Unknown',
|
||||
date: match.demoDate,
|
||||
matchType,
|
||||
id : m.id,
|
||||
map : m.map ?? 'Unknown',
|
||||
date : m.demoDate?.toISOString() ?? '',
|
||||
matchType : m.matchType ?? 'community',
|
||||
|
||||
score,
|
||||
roundCount,
|
||||
roundCount: m.roundCount,
|
||||
|
||||
rankOld,
|
||||
rankNew,
|
||||
rankChange,
|
||||
|
||||
kills,
|
||||
deaths,
|
||||
kdr,
|
||||
winnerTeam: match.winnerTeam ?? null,
|
||||
team: playerTeam,
|
||||
|
||||
winnerTeam: m.winnerTeam ?? null,
|
||||
team : playerTeam, // „CT“ oder „T“
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('[API] Fehler beim Laden der Matches:', error)
|
||||
} catch (err) {
|
||||
console.error('[API] Fehler beim Laden der Matches:', err)
|
||||
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
|
||||
children: React.ReactNode
|
||||
activeDragItem: Player | null
|
||||
saveSuccess?: boolean
|
||||
}
|
||||
|
||||
export function DroppableZone({
|
||||
id,
|
||||
label,
|
||||
children,
|
||||
saveSuccess = false,
|
||||
}: DroppableZoneProps) {
|
||||
const { isOver, setNodeRef } = useDroppable({ id })
|
||||
|
||||
@ -31,9 +33,29 @@ export function DroppableZone({
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-md font-semibold text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-md font-semibold text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
</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 */}
|
||||
<div ref={setNodeRef} className={zoneClasses}>
|
||||
|
||||
@ -18,6 +18,7 @@ import EditMatchPlayersModal from './EditMatchPlayersModal'
|
||||
import type { EditSide } from './EditMatchPlayersModal' // 'A' | 'B'
|
||||
|
||||
import type { Match, MatchPlayer } from '../types/match'
|
||||
import Button from './Button'
|
||||
|
||||
/* ─────────────────── Hilfsfunktionen ────────────────────────── */
|
||||
const kdr = (k?: number, d?: number) =>
|
||||
@ -165,13 +166,14 @@ export function MatchDetails ({ match }: { match: Match }) {
|
||||
</h2>
|
||||
|
||||
{canEditA && isFutureMatch && (
|
||||
<button
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={() => setEditSide('A')}
|
||||
className="px-3 py-1.5 text-sm rounded-lg
|
||||
bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
Spieler bearbeiten
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -186,13 +188,14 @@ export function MatchDetails ({ match }: { match: Match }) {
|
||||
</h2>
|
||||
|
||||
{canEditB && isFutureMatch && (
|
||||
<button
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={() => setEditSide('B')}
|
||||
className="px-3 py-1.5 text-sm rounded-lg
|
||||
bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
Spieler bearbeiten
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{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}
|
||||
</label>
|
||||
)}
|
||||
|
||||
@ -82,7 +82,7 @@ export default function TeamCard({
|
||||
className="
|
||||
p-4 border rounded-lg bg-white dark:bg-neutral-800
|
||||
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 */}
|
||||
@ -111,8 +111,9 @@ export default function TeamCard({
|
||||
{adminMode ? (
|
||||
<Button
|
||||
title="Verwalten"
|
||||
size="sm"
|
||||
size="md"
|
||||
color="blue"
|
||||
variant='solid'
|
||||
onClick={e => {
|
||||
e.stopPropagation() // ▼ Navigation hier unterbinden
|
||||
router.push(`/admin/teams/${data.id}`)
|
||||
|
||||
@ -73,6 +73,7 @@ export default function TeamMemberView({
|
||||
const [logoPreview, setLogoPreview] = useState<string | null>(null)
|
||||
const [logoFile, setLogoFile] = useState<File | null>(null)
|
||||
const [teamState, setTeamState] = useState<Team | null>(team)
|
||||
const [saveSuccess, setSaveSuccess] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user?.steamId) {
|
||||
@ -205,6 +206,8 @@ export default function TeamMemberView({
|
||||
setactivePlayers(newActive)
|
||||
setInactivePlayers(newInactive)
|
||||
await updateTeamMembers(team!.id, newActive, newInactive)
|
||||
setSaveSuccess(true)
|
||||
setTimeout(() => setSaveSuccess(false), 3000) // 3 Sekunden sichtbar
|
||||
}
|
||||
|
||||
const confirmKick = async () => {
|
||||
@ -488,13 +491,13 @@ export default function TeamMemberView({
|
||||
|
||||
<DndContext collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<div className="space-y-8">
|
||||
<DroppableZone id="active" label={`Aktives Team (${activePlayers.length} / 5)`} activeDragItem={activeDragItem}>
|
||||
<DroppableZone id="active" label={`Aktives Team (${activePlayers.length} / 5)`} activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
||||
<SortableContext items={activePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||
{renderMemberList(activePlayers)}
|
||||
</SortableContext>
|
||||
</DroppableZone>
|
||||
|
||||
<DroppableZone id="inactive" label="Inaktives Team" activeDragItem={activeDragItem}>
|
||||
<DroppableZone id="inactive" label="Inaktives Team" activeDragItem={activeDragItem} saveSuccess={saveSuccess}>
|
||||
<SortableContext items={inactivePlayers.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||
{renderMemberList(inactivePlayers)}
|
||||
{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 Image from 'next/image'
|
||||
import Switch from '../Switch'
|
||||
import CommunityMatchList from '../CommunityMatchList'
|
||||
import Card from '../Card'
|
||||
|
||||
function getRoundedDate() {
|
||||
const now = new Date()
|
||||
@ -27,16 +29,12 @@ function getTeamLogo(logo?: string | null) {
|
||||
}
|
||||
|
||||
export default function MatchesAdminManager() {
|
||||
const { data: session } = useSession()
|
||||
const [teams, setTeams] = useState<any[]>([])
|
||||
const [matches, setMatches] = useState<any[]>([])
|
||||
const [teamAId, setTeamAId] = useState('')
|
||||
const [teamBId, setTeamBId] = useState('')
|
||||
const [title, setTitle] = useState('')
|
||||
const [titleManuallySet, setTitleManuallySet] = useState(false)
|
||||
const [description, setDescription] = useState('')
|
||||
const [matchDate, setMatchDate] = useState(getRoundedDate())
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
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())
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="max-w-4xl mx-auto py-8 px-4 space-y-4">
|
||||
<div className="max-w-4xl mx-auto px-4 flex justify-between items-center mb-4">
|
||||
<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>
|
||||
</>
|
||||
<Card maxWidth='auto'>
|
||||
<CommunityMatchList matchType="community" />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import Modal from '@/app/components/Modal'
|
||||
import Input from '@/app/components/Input'
|
||||
import TeamCard from '@/app/components/TeamCard'
|
||||
import type { Team } from '@/app/types/team'
|
||||
import LoadingSpinner from '../../LoadingSpinner'
|
||||
|
||||
export default function AdminTeamsView() {
|
||||
/* ────────────────────────── Session ─────────────────────────── */
|
||||
@ -75,7 +76,7 @@ export default function AdminTeamsView() {
|
||||
if (loading) {
|
||||
return (
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Lade Teams …
|
||||
<LoadingSpinner />
|
||||
</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 Chart from '@/app/components/Chart'
|
||||
import { MatchStats } from '@/app/types/match'
|
||||
import Card from './Card'
|
||||
import UserClips from './UserClips'
|
||||
import Card from '../../../Card'
|
||||
import UserClips from '../../../UserClips'
|
||||
|
||||
type MatchStatsProps = {
|
||||
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
|
||||
import UserMatchesTable from '@/app/components/UserMatchesTable'
|
||||
import UserMatchesList from '@/app/components/profile/[steamId]/matches/UserMatchesList'
|
||||
import Card from '@/app/components/Card'
|
||||
|
||||
export default function MatchesPage({ params }: { params: { steamId: string } }) {
|
||||
@ -10,7 +10,7 @@ export default function MatchesPage({ params }: { params: { steamId: string } })
|
||||
</h1>
|
||||
|
||||
<Card maxWidth="auto">
|
||||
<UserMatchesTable steamId={params.steamId} />
|
||||
<UserMatchesList steamId={params.steamId} />
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
// /app/profile/[steamId]/matches/page.tsx
|
||||
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 } }) {
|
||||
return (
|
||||
<Card maxWidth="auto">
|
||||
<UserMatchesTable steamId={params.steamId} />
|
||||
<UserMatchesList steamId={params.steamId} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// /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'
|
||||
|
||||
async function getStats(steamId: string) {
|
||||
|
||||
@ -6,6 +6,8 @@ import { useEffect, useState } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import Switch from '@/app/components/Switch'
|
||||
import Button from '../components/Button'
|
||||
import CommunityMatchList from '../components/CommunityMatchList'
|
||||
import Card from '../components/Card'
|
||||
|
||||
type Match = {
|
||||
id: string
|
||||
@ -15,10 +17,6 @@ type Match = {
|
||||
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() {
|
||||
const { data: session } = useSession()
|
||||
const [matches, setMatches] = useState<Match[]>([])
|
||||
@ -41,83 +39,8 @@ export default function MatchesPage() {
|
||||
: 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 text-gray-700 dark:text-neutral-300">Geplante Matches</h1>
|
||||
|
||||
{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>
|
||||
<Card maxWidth='auto'>
|
||||
<CommunityMatchList matchType="community" />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,70 +1,80 @@
|
||||
// src/app/types/match.ts
|
||||
|
||||
import { Player } from './team'
|
||||
|
||||
export type Match = {
|
||||
id: string
|
||||
title: string
|
||||
demoDate: Date
|
||||
/* Basis-Infos ---------------------------------------------------- */
|
||||
id : string
|
||||
title : string
|
||||
demoDate : string // ⇐ Backend kommt als ISO-String
|
||||
description?: string
|
||||
map: string
|
||||
matchType: string
|
||||
roundCount: number
|
||||
scoreA?: number | null
|
||||
scoreB?: number | null
|
||||
map : string
|
||||
matchType : 'premier' | 'competitive' | 'community' | string
|
||||
roundCount : number
|
||||
|
||||
/* Ergebnis ------------------------------------------------------- */
|
||||
scoreA? : number | null
|
||||
scoreB? : number | null
|
||||
/** CT | T | Draw | null – null, solange noch kein Ergebnis vorliegt */
|
||||
winnerTeam? : 'CT' | 'T' | 'Draw' | null
|
||||
|
||||
/* Teams ---------------------------------------------------------- */
|
||||
teamA: {
|
||||
id: string
|
||||
name: string
|
||||
logo?: string | null
|
||||
id : string
|
||||
name : string
|
||||
logo? : string | null
|
||||
leader?: string | null
|
||||
players: MatchPlayer[]
|
||||
}
|
||||
teamB: {
|
||||
id: string
|
||||
name: string
|
||||
logo?: string | null
|
||||
id : string
|
||||
name : string
|
||||
logo? : string | null
|
||||
leader?: string | null
|
||||
players: MatchPlayer[]
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- */
|
||||
export type MatchPlayer = {
|
||||
user: Player
|
||||
user : Player
|
||||
stats?: {
|
||||
kills: number
|
||||
deaths: number
|
||||
assists: number
|
||||
totalDamage: number
|
||||
utilityDamage: number
|
||||
headshotPct: number
|
||||
flashAssists: number
|
||||
mvps: number
|
||||
knifeKills: number
|
||||
zeusKills: number
|
||||
wallbangKills: number
|
||||
smokeKills: number
|
||||
headshots: number
|
||||
noScopes: number
|
||||
blindKills: Number
|
||||
rankOld: number
|
||||
rankNew: number
|
||||
rankChange: number
|
||||
k1: number
|
||||
k2: number
|
||||
k3: number
|
||||
k4: number
|
||||
k5: number
|
||||
kills : number
|
||||
deaths : number
|
||||
assists : number
|
||||
totalDamage : number
|
||||
utilityDamage : number
|
||||
headshotPct : number
|
||||
flashAssists : number
|
||||
mvps : number
|
||||
knifeKills : number
|
||||
zeusKills : number
|
||||
wallbangKills : number
|
||||
smokeKills : number
|
||||
headshots : number
|
||||
noScopes : number
|
||||
blindKills : number
|
||||
rankOld : number
|
||||
rankNew : number
|
||||
rankChange : number
|
||||
k1 : number
|
||||
k2 : number
|
||||
k3 : number
|
||||
k4 : number
|
||||
k5 : number
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- */
|
||||
export type MatchStats = {
|
||||
date: string
|
||||
kills: number
|
||||
deaths: number
|
||||
assists: number
|
||||
headshotPct: number,
|
||||
totalDamage: number,
|
||||
map: string,
|
||||
matchType: 'premier' | 'competitive' | 'community' // falls du auch andere hast
|
||||
rankNew: number | null
|
||||
rankOld?: number | null
|
||||
date : string // ISO
|
||||
kills : number
|
||||
deaths : number
|
||||
assists : number
|
||||
headshotPct : number
|
||||
totalDamage : number
|
||||
map : string
|
||||
matchType : 'premier' | 'competitive' | 'community'
|
||||
rankNew : number | null
|
||||
rankOld? : number | null
|
||||
}
|
||||
|
||||
|
||||
@ -15,4 +15,5 @@ export type Team = {
|
||||
leader?: string | null
|
||||
activePlayers? : Player[]
|
||||
inactivePlayers?: Player[]
|
||||
invitedPlayers?: Player[]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user