This commit is contained in:
Linrador 2025-09-20 21:28:10 +02:00
parent 237be94ebe
commit 6543210eba
67 changed files with 1233 additions and 442 deletions

View File

@ -159,6 +159,9 @@
schedule Schedule? schedule Schedule?
readyAcceptances MatchReady[] @relation("MatchReadyMatch") readyAcceptances MatchReady[] @relation("MatchReadyMatch")
cs2MatchId Int? // die in die JSON geschriebene matchid
exportedAt DateTime? // wann die JSON exportiert wurde
} }
model MatchPlayer { model MatchPlayer {

View File

@ -1,4 +1,4 @@
// src/app/admin/page.tsx // /src/app/admin/page.tsx
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
export default function AdminRedirectPage() { export default function AdminRedirectPage() {

View File

@ -1,4 +1,4 @@
// src/app/(admin)/admin/teams/[teamId]/TeamAdminClient.tsx // /src/app/(admin)/admin/teams/[teamId]/TeamAdminClient.tsx
'use client' 'use client'
import { useCallback, useEffect, useState, useRef } from 'react' import { useCallback, useEffect, useState, useRef } from 'react'

View File

@ -1,3 +1,5 @@
// /src/app/admin/teams/page.tsx
'use client' 'use client'
import Card from '@/app/components/Card' import Card from '@/app/components/Card'

View File

@ -1,4 +1,4 @@
// /app/api/matches/[id]/_builders.ts // /src/app/api/matches/[id]/_builders.ts
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'
/** Klein, konsistent, Frontend-freundlich */ /** Klein, konsistent, Frontend-freundlich */

View File

@ -1,4 +1,4 @@
// /app/api/matches/[matchId]/mapvote/admin-edit/route.ts // /src/app/api/matches/[matchId]/mapvote/admin-edit/route.ts
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth' import { authOptions } from '@/app/lib/auth'

View File

@ -1,4 +1,4 @@
// /app/api/matches/[id]/mapvote/reset/route.ts // /src/app/api/matches/[id]/mapvote/reset/route.ts
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth' import { authOptions } from '@/app/lib/auth'

View File

@ -1,4 +1,4 @@
// /app/api/matches/[matchId]/mapvote/route.ts // /src/app/api/matches/[matchId]/mapvote/route.ts
import { NextResponse, NextRequest } from 'next/server' import { NextResponse, NextRequest } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
@ -22,6 +22,15 @@ const ACTION_MAP: Record<MapVoteAction, 'ban'|'pick'|'decider'> = {
const sleep = (ms: number) => new Promise<void>(res => setTimeout(res, ms)); const sleep = (ms: number) => new Promise<void>(res => setTimeout(res, ms));
async function unloadCurrentMatch() {
// einige MatchZy Builds nutzen "matchzy_unloadmatch",
// andere trennen zwischen cancel/end. Der Unload reicht meist.
await sendServerCommand('matchzy_unloadmatch')
// optional „end/cancel“ hinterher, falls dein Build es erfordert:
// await sendServerCommand('matchzy_cancelmatch')
await sleep(500) // Server eine halbe Sekunde Luft lassen
}
function makeRandomMatchId() { function makeRandomMatchId() {
try { try {
// 910-stellige ID (>= 100_000_000) Obergrenze exklusiv // 910-stellige ID (>= 100_000_000) Obergrenze exklusiv
@ -361,6 +370,34 @@ function collectParticipants(match: any): string[] {
]) ])
} }
async function persistMatchPlayers(match: any) {
// Teilnehmer ermitteln (du hast schon collectParticipants)
const participants = collectParticipants(match); // string[] der steamIds
// teamId pro Spieler bestimmen (A oder B), sonst null
const aIds = new Set((match.teamAUsers ?? []).map((u: any) => String(u?.steamId)).filter(Boolean));
const bIds = new Set((match.teamBUsers ?? []).map((u: any) => String(u?.steamId)).filter(Boolean));
const ops = participants.map((steamId) => {
const onTeamA = aIds.has(String(steamId));
const onTeamB = bIds.has(String(steamId));
const teamId =
onTeamA ? match.teamA?.id
: onTeamB ? match.teamB?.id
: null;
// Upsert je Spieler fürs Match
return prisma.matchPlayer.upsert({
where: { matchId_steamId: { matchId: match.id, steamId } },
update: { teamId }, // falls sich die Team-Zuordnung ändert
create: { matchId: match.id, steamId, teamId },
});
});
await prisma.$transaction(ops);
}
/* ---------- Export-Helfer ---------- */ /* ---------- Export-Helfer ---------- */
@ -452,10 +489,12 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
const chosen = (sLike.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map) const chosen = (sLike.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
if (chosen.length < bestOf) return if (chosen.length < bestOf) return
// ⬇️ JSON bauen (enthält cs2MatchId/rndId)
const json = buildMatchJson(mLike, sLike) const json = buildMatchJson(mLike, sLike)
const jsonStr = JSON.stringify(json, null, 2) const jsonStr = JSON.stringify(json, null, 2)
const filename = `${match.id}.json` const filename = `${match.id}.json`
// --- SFTP Upload wie gehabt ---
const url = process.env.PTERO_SERVER_SFTP_URL || '' const url = process.env.PTERO_SERVER_SFTP_URL || ''
const user = process.env.PTERO_SERVER_SFTP_USER const user = process.env.PTERO_SERVER_SFTP_USER
const pass = process.env.PTERO_SERVER_SFTP_PASSWORD const pass = process.env.PTERO_SERVER_SFTP_PASSWORD
@ -484,16 +523,46 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
console.log(`[mapvote] Export OK → ${remotePath}`) console.log(`[mapvote] Export OK → ${remotePath}`)
// 👇 NACH ERFOLGREICHEM UPLOAD: Match in CS2-Plugin laden // erst aktuelles Match beenden/entladen …
// Laut Vorgabe nur die JSON-Datei als Argument übergeben: await unloadCurrentMatch()
// … dann das neue laden
await sendServerCommand(`matchzy_loadmatch ${filename}`) await sendServerCommand(`matchzy_loadmatch ${filename}`)
// (Falls dein Plugin den absoluten Pfad erwartet, nimm stattdessen: `matchzy_loadmatch ${remotePath}`)
// Spieler persistieren + cs2MatchId speichern wie gehabt
await persistMatchPlayers(match)
if (typeof json.matchid === 'number') {
await prisma.match.update({
where: { id: match.id },
data: { cs2MatchId: json.matchid, exportedAt: new Date() },
})
} else {
await prisma.match.update({
where: { id: match.id },
data: { exportedAt: new Date() },
})
}
// ⬇️ OPTIONAL: cs2MatchId + exportedAt im Match speichern
if (typeof json.matchid === 'number') {
await prisma.match.update({
where: { id: match.id },
data: { cs2MatchId: json.matchid, exportedAt: new Date() },
})
} else {
await prisma.match.update({
where: { id: match.id },
data: { exportedAt: new Date() },
})
}
} catch (err) { } catch (err) {
console.error('[mapvote] Export fehlgeschlagen:', err) console.error('[mapvote] Export fehlgeschlagen:', err)
} }
} }
/* ---------- kleine Helfer für match-ready Payload ---------- */ /* ---------- kleine Helfer für match-ready Payload ---------- */
function deriveChosenSteps(vote: any) { function deriveChosenSteps(vote: any) {

View File

@ -1,4 +1,4 @@
// /app/api/matches/[id]/route.ts // /src/app/api/matches/[id]/route.ts
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'

View File

@ -1,4 +1,4 @@
// /app/api/matches/create/route.ts // /src/app/api/matches/create/route.ts
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth' import { authOptions } from '@/app/lib/auth'

View File

@ -1,7 +1,8 @@
import { prisma } from '@/app/lib/prisma' // /api/notifications/route.ts
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth' import { authOptions } from '@/app/lib/auth'
import { NextResponse, type NextRequest } from 'next/server' import { prisma } from '@/app/lib/prisma'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions(req)) const session = await getServerSession(authOptions(req))
@ -11,9 +12,19 @@ export async function GET(req: NextRequest) {
} }
const notifications = await prisma.notification.findMany({ const notifications = await prisma.notification.findMany({
where: { steamId: session.user.steamId }, where: {
steamId: session.user.steamId,
},
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
take: 10, select: {
id: true,
title: true,
message: true,
read: true,
actionType: true,
actionData: true,
createdAt: true,
},
}) })
return NextResponse.json({ notifications }) return NextResponse.json({ notifications })

View File

@ -1,31 +0,0 @@
// /api/notifications/user/route.ts
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth'
import { prisma } from '@/app/lib/prisma'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions(req))
if (!session?.user?.steamId) {
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
}
const notifications = await prisma.notification.findMany({
where: {
steamId: session.user.steamId,
},
orderBy: { createdAt: 'desc' },
select: {
id: true,
title: true,
message: true,
read: true,
actionType: true,
actionData: true,
createdAt: true,
},
})
return NextResponse.json({ notifications })
}

View File

@ -1,4 +1,4 @@
// /app/api/schedule/route.ts // /src/app/api/schedule/route.ts
'use server' 'use server'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'

View File

@ -1,4 +1,4 @@
// /app/api/team/add-players/route.ts // /src/app/api/team/add-players/route.ts
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } 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'

View File

@ -1,4 +1,4 @@
// /app/api/team/delete/route.ts // /src/app/api/team/delete/route.ts
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth' import { authOptions } from '@/app/lib/auth'

View File

@ -1,4 +1,4 @@
// src/app/api/team/kick/route.ts // /src/app/api/team/kick/route.ts
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } 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'

View File

@ -1,4 +1,4 @@
// src/app/api/team/leave/route.ts // /src/app/api/team/leave/route.ts
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'
import { removePlayerFromMatches } from '@/app/lib/removePlayerFromMatches' import { removePlayerFromMatches } from '@/app/lib/removePlayerFromMatches'

View File

@ -1,4 +1,4 @@
// /app/api/team/rename/route.ts // /src/app/api/team/rename/route.ts
import { NextResponse, type NextRequest } 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'

View File

@ -1,4 +1,4 @@
// src/app/api/team/request-join/route.ts // /src/app/api/team/request-join/route.ts
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'

View File

@ -1,4 +1,4 @@
// /app/api/team/transfer-leader/route.ts // /src/app/api/team/transfer-leader/route.ts
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
import { sendServerSSEMessage } from '@/app/lib/sse-server-client' import { sendServerSSEMessage } from '@/app/lib/sse-server-client'

View File

@ -1,4 +1,4 @@
// src/app/api/teams/route.ts // /src/app/api/teams/route.ts
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'
import type { Player } from '@/app/types/team' import type { Player } from '@/app/types/team'
@ -41,14 +41,16 @@ export async function GET() {
id: t.id, id: t.id,
name: t.name, name: t.name,
logo: t.logo, logo: t.logo,
leader: t.leaderId, leaderId: t.leaderId,
createdAt: t.createdAt, createdAt: t.createdAt,
activePlayers: t.activePlayers .map(id => byId[id]).filter(Boolean) as Player[], activePlayers: t.activePlayers .map(id => byId[id]).filter(Boolean) as Player[],
inactivePlayers:t.inactivePlayers.map(id => byId[id]).filter(Boolean) as Player[], inactivePlayers:t.inactivePlayers.map(id => byId[id]).filter(Boolean) as Player[],
})) }))
// HIER: direkt das Array zurückgeben return NextResponse.json(
return NextResponse.json(result, { headers: { 'Cache-Control': 'no-store' } }) { items: result, hasMore: false },
{ headers: { 'Cache-Control': 'no-store' } }
)
} catch (err) { } catch (err) {
console.error('GET /api/teams failed:', err) console.error('GET /api/teams failed:', err)
return NextResponse.json({ message: 'Interner Serverfehler' }, { status: 500 }) return NextResponse.json({ message: 'Interner Serverfehler' }, { status: 500 })

View File

@ -1,4 +1,4 @@
// /app/api/user/[steamId]/matches/route.ts // /src/app/api/user/[steamId]/matches/route.ts
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'

View File

@ -1,4 +1,4 @@
// src/app/api/user/invitations/route.ts // /src/app/api/user/invitations/route.ts
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth' import { authOptions } from '@/app/lib/auth'

View File

@ -1,4 +1,4 @@
// src/app/api/user/route.ts // /src/app/api/user/route.ts
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth' import { authOptions } from '@/app/lib/auth'

View File

@ -10,7 +10,10 @@ type ButtonProps = {
modalId?: string modalId?: string
color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent' color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent'
variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link' variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link'
/** Steuert NUR Höhe/Abstände */
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full' size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
/** Optionale Schriftgröße */
textSize?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl'
className?: string className?: string
dropDirection?: 'up' | 'down' | 'auto' dropDirection?: 'up' | 'down' | 'auto'
disabled?: boolean disabled?: boolean
@ -27,6 +30,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
color = 'blue', color = 'blue',
variant = 'solid', variant = 'solid',
size = 'md', size = 'md',
textSize = 'sm',
className, className,
dropDirection = 'down', dropDirection = 'down',
disabled = false, disabled = false,
@ -49,18 +53,31 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
} }
: {} : {}
const sizeClasses: Record<string, string> = { // Feste Höhen sorgen für vertikale Zentrierung
xs: 'py-1 px-2', const sizeClasses: Record<NonNullable<ButtonProps['size']>, string> = {
sm: 'py-2 px-3', xs: 'h-7 px-2',
md: 'py-3 px-4', sm: 'h-8 px-3',
lg: 'p-4 sm:p-5', md: 'h-9 px-4',
xl: 'py-6 px-8 text-lg', lg: 'h-10 px-5',
full: 'py-6 px-8 text-lg w-full', xl: 'h-12 px-6',
full: 'h-12 px-6 w-full',
}
// Nur Textgröße
const textSizeClasses: Record<NonNullable<ButtonProps['textSize']>, string> = {
xs: 'text-xs',
sm: 'text-sm',
base: 'text-base',
lg: 'text-lg',
xl: 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
} }
const base = ` const base = `
${sizeClasses[size] || sizeClasses['md']} ${sizeClasses[size] || sizeClasses['md']}
inline-flex items-center gap-x-2 text-sm font-medium rounded-lg inline-flex items-center gap-x-2 ${textSizeClasses[textSize] || 'text-sm'}
font-medium rounded-lg leading-none
focus:outline-hidden disabled:opacity-50 disabled:cursor-not-allowed focus:outline-hidden disabled:opacity-50 disabled:cursor-not-allowed
` `
@ -79,15 +96,15 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
gray: 'border border-gray-200 text-gray-500 hover:border-gray-600 hover:text-gray-600 focus:border-gray-600 focus:text-gray-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600', gray: 'border border-gray-200 text-gray-500 hover:border-gray-600 hover:text-gray-600 focus:border-gray-600 focus:text-gray-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600',
teal: 'border border-teal-200 text-teal-500 hover:border-teal-600 hover:text-teal-600 focus:border-teal-600 focus:text-teal-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600', teal: 'border border-teal-200 text-teal-500 hover:border-teal-600 hover:text-teal-600 focus:border-teal-600 focus:text-teal-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600',
green: 'border border-green-200 text-green-500 hover:border-green-600 hover:text-green-600 focus:border-green-600 focus:text-green-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600', green: 'border border-green-200 text-green-500 hover:border-green-600 hover:text-green-600 focus:border-green-600 focus:text-green-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600',
transparent: 'border border-transparent-200 text-transparent-500 hover:border-transparent-600 hover:text-transparent-600 focus:border-transparent-600 focus:text-transparent-600 dark:border-neutral-700 dark:text-neutral-400 dark:hover:text-white dark:hover:border-neutral-600 dark:focus:text-white dark:focus:border-neutral-600', transparent: 'border border-white/20 bg-transparent text-white shadow-2xs hover:bg-white/15 focus:bg-white/15 dark:bg-transparent dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700/50 dark:focus:bg-neutral-700/50',
}, },
ghost: { ghost: {
blue: 'border border-transparent text-blue-600 hover:bg-blue-100 hover:text-blue-800 focus:bg-blue-100 focus:text-blue-800 dark:text-blue-500 dark:hover:bg-blue-800/30 dark:hover:text-blue-400 dark:focus:bg-blue-800/30 dark:focus:text-blue-400', blue: 'border border-transparent text-blue-600 hover:bg-blue-100 hover:text-blue-800 focus:bg-blue-100 focus:text-blue-800 dark:text-blue-500 dark:hover:bg-blue-800/30 dark:hover:text-blue-400 dark:focus:bg-blue-800/30 dark:focus:text-blue-400',
red: 'border border-transparent text-red-600 hover:bg-red-100 hover:text-red-800 focus:bg-red-100 focus:text-red-800 dark:text-red-500 dark:hover:bg-red-800/30 dark:hover:text-red-400 dark:focus:bg-red-800/30 dark:focus:text-red-400', red: 'border border-transparent text-red-600 hover:bg-red-100 hover:text-red-800 focus:bg-red-100 focus:text-red-800 dark:text-red-500 dark:hover:bg-red-800/30 dark:hover:text-red-400 dark:focus:bg-red-800/30 dark:focus:text-red-400',
gray: 'border border-transparent text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white', gray: 'border border-transparent text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
teal: 'border border-transparent text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white', teal: 'border border-transparent text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
green: 'border border-transparent text-green-600 hover:bg-green-100 hover:text-green-800 focus:bg-green-100 focus:text-green-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white', green: 'border border-transparent text-green-600 hover:bg-green-100 hover:text-green-800 focus:bg-green-100 focus:text-green-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:focus:text-white',
transparent: 'border border-transparent text-transparent-600 hover:bg-transparent-100 focus:bg-transparent-100 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white', transparent: 'border border-transparent text-white hover:bg-white/10 focus:bg-white/10 dark:text-white',
}, },
soft: { soft: {
blue: 'bg-blue-100 text-blue-800 hover:bg-blue-200 focus:bg-blue-200 dark:text-blue-400 dark:hover:bg-blue-900 dark:focus:bg-blue-900', blue: 'bg-blue-100 text-blue-800 hover:bg-blue-200 focus:bg-blue-200 dark:text-blue-400 dark:hover:bg-blue-900 dark:focus:bg-blue-900',
@ -103,7 +120,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
gray: 'border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700', gray: 'border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
teal: 'border border-teal-200 bg-white text-teal-800 shadow-2xs hover:bg-teal-50 focus:bg-teal-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700', teal: 'border border-teal-200 bg-white text-teal-800 shadow-2xs hover:bg-teal-50 focus:bg-teal-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
green: 'border border-green-200 bg-white text-green-800 shadow-2xs hover:bg-green-50 focus:bg-green-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700', green: 'border border-green-200 bg-white text-green-800 shadow-2xs hover:bg-green-50 focus:bg-green-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
transparent: 'border border-transparent-200 bg-white text-transparent-800 shadow-2xs hover:bg-transparent-50 focus:bg-transparent-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700', transparent: 'border border-white/20 bg-transparent text-white shadow-2xs hover:bg-white/15 focus:bg-white/15 dark:bg-transparent dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700/50 dark:focus:bg-neutral-700/50',
}, },
link: { link: {
blue: 'border border-transparent text-blue-600 hover:text-blue-800 focus:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400 dark:focus:text-blue-400', blue: 'border border-transparent text-blue-600 hover:text-blue-800 focus:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400 dark:focus:text-blue-400',
@ -139,12 +156,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
const dropdownHeight = 200 const dropdownHeight = 200
const spaceBelow = window.innerHeight - rect.bottom const spaceBelow = window.innerHeight - rect.bottom
const spaceAbove = rect.top const spaceAbove = rect.top
setDirection(spaceBelow < dropdownHeight && spaceAbove > dropdownHeight ? 'up' : 'down')
if (spaceBelow < dropdownHeight && spaceAbove > dropdownHeight) {
setDirection('up')
} else {
setDirection('down')
}
}) })
} }
}, [open, dropDirection]) }, [open, dropDirection])
@ -171,7 +183,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
className="animate-spin inline-block size-4 border-[3px] border-current border-t-transparent rounded-full mr-2" className="animate-spin inline-block size-4 border-[3px] border-current border-t-transparent rounded-full mr-2"
role="status" role="status"
aria-label="loading" aria-label="loading"
></span> />
)} )}
{children ?? title} {children ?? title}
</button> </button>

View File

@ -1,4 +1,4 @@
// /app/components/LoadingSpinner.tsx // /src/app/components/LoadingSpinner.tsx
'use client' 'use client'

View File

@ -1,4 +1,4 @@
// /app/components/MapVoteBanner.tsx // /src/app/components/MapVoteBanner.tsx
'use client' 'use client'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
@ -24,7 +24,6 @@ function formatCountdown(ms: number) {
const pad = (n:number)=>String(n).padStart(2,'0') const pad = (n:number)=>String(n).padStart(2,'0')
return `${h}:${pad(m)}:${pad(s)}` return `${h}:${pad(m)}:${pad(s)}`
} }
function formatLead(minutes: number) { function formatLead(minutes: number) {
if (!Number.isFinite(minutes) || minutes <= 0) return 'zum Matchbeginn' if (!Number.isFinite(minutes) || minutes <= 0) return 'zum Matchbeginn'
const h = Math.floor(minutes / 60) const h = Math.floor(minutes / 60)
@ -35,11 +34,7 @@ function formatLead(minutes: number) {
} }
export default function MapVoteBanner({ export default function MapVoteBanner({
match, match, initialNow, matchBaseTs, sseOpensAtTs, sseLeadMinutes,
initialNow,
matchBaseTs,
sseOpensAtTs,
sseLeadMinutes,
}: Props) { }: Props) {
const router = useRouter() const router = useRouter()
const { data: session } = useSession() const { data: session } = useSession()
@ -50,16 +45,11 @@ export default function MapVoteBanner({
const [leadOverride, setLeadOverride] = useState<number | null>(null) const [leadOverride, setLeadOverride] = useState<number | null>(null)
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null) const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null)
// ⚠️ Hydration-sicher: auf dem Server rendern wir ein statisches Placeholder
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) }, []) useEffect(() => { setMounted(true) }, [])
// clientseitiger Ticker
const [now, setNow] = useState(initialNow) const [now, setNow] = useState(initialNow)
useEffect(() => { useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id) }, [])
const id = setInterval(() => setNow(Date.now()), 1000)
return () => clearInterval(id)
}, [])
const load = useCallback(async () => { const load = useCallback(async () => {
try { try {
@ -78,16 +68,11 @@ export default function MapVoteBanner({
} }
}, [match.id]) }, [match.id])
// initial + bei Meta-Änderungen
useEffect(() => { load() }, [load]) useEffect(() => { load() }, [load])
useEffect(() => { load() }, [match.matchDate, match.demoDate, match.bestOf, load]) useEffect(() => { load() }, [match.matchDate, match.demoDate, match.bestOf, load])
const matchDateTs = useMemo( const matchDateTs = useMemo(() => (typeof matchBaseTs === 'number' ? matchBaseTs : null), [matchBaseTs])
() => (typeof matchBaseTs === 'number' ? matchBaseTs : null),
[matchBaseTs]
)
// SSE: nur map-vote-updated & Co. beachten
useEffect(() => { useEffect(() => {
if (!lastEvent) return if (!lastEvent) return
const { type } = lastEvent as any const { type } = lastEvent as any
@ -107,7 +92,6 @@ export default function MapVoteBanner({
? (typeof evt.opensAt === 'string' ? evt.opensAt : new Date(evt.opensAt).toISOString()) ? (typeof evt.opensAt === 'string' ? evt.opensAt : new Date(evt.opensAt).toISOString())
: undefined : undefined
// Sofort lokale Overrides setzen
if (nextOpensAtISO) { if (nextOpensAtISO) {
setOpensAtOverride(new Date(nextOpensAtISO).getTime()) setOpensAtOverride(new Date(nextOpensAtISO).getTime())
} else if (Number.isFinite(parsedLead) && matchDateTs != null) { } else if (Number.isFinite(parsedLead) && matchDateTs != null) {
@ -115,7 +99,6 @@ export default function MapVoteBanner({
} }
if (Number.isFinite(parsedLead)) setLeadOverride(parsedLead as number) if (Number.isFinite(parsedLead)) setLeadOverride(parsedLead as number)
// sichtbares Mergen (für UI-Texte)
if (nextOpensAtISO !== undefined || Number.isFinite(parsedLead)) { if (nextOpensAtISO !== undefined || Number.isFinite(parsedLead)) {
setState(prev => ({ setState(prev => ({
...(prev ?? {} as any), ...(prev ?? {} as any),
@ -127,7 +110,6 @@ export default function MapVoteBanner({
} }
}, [lastEvent, match.id, matchDateTs, load]) }, [lastEvent, match.id, matchDateTs, load])
// Öffnet wann? (Priorität: Parent-SSE → lokale SSE → Server → Fallback)
const opensAt = useMemo(() => { const opensAt = useMemo(() => {
if (typeof sseOpensAtTs === 'number') return sseOpensAtTs if (typeof sseOpensAtTs === 'number') return sseOpensAtTs
if (opensAtOverride != null) return opensAtOverride if (opensAtOverride != null) return opensAtOverride
@ -139,11 +121,8 @@ export default function MapVoteBanner({
return matchDateTs - lead * 60_000 return matchDateTs - lead * 60_000
}, [sseOpensAtTs, opensAtOverride, state?.opensAt, matchDateTs, initialNow, sseLeadMinutes, leadOverride, state?.leadMinutes]) }, [sseOpensAtTs, opensAtOverride, state?.opensAt, matchDateTs, initialNow, sseLeadMinutes, leadOverride, state?.leadMinutes])
// „startet X vor Matchbeginn“
const leadMinutes = useMemo(() => { const leadMinutes = useMemo(() => {
if (matchDateTs != null && opensAt != null) { if (matchDateTs != null && opensAt != null) return Math.max(0, Math.round((matchDateTs - opensAt) / 60_000))
return Math.max(0, Math.round((matchDateTs - opensAt) / 60_000))
}
if (typeof sseLeadMinutes === 'number') return sseLeadMinutes if (typeof sseLeadMinutes === 'number') return sseLeadMinutes
if (leadOverride != null) return leadOverride if (leadOverride != null) return leadOverride
if (Number.isFinite(state?.leadMinutes)) return state!.leadMinutes as number if (Number.isFinite(state?.leadMinutes)) return state!.leadMinutes as number
@ -152,6 +131,8 @@ export default function MapVoteBanner({
const isOpen = mounted && now >= opensAt const isOpen = mounted && now >= opensAt
const msToOpen = Math.max(opensAt - now, 0) const msToOpen = Math.max(opensAt - now, 0)
const isLocked = !!state?.locked
const isVotingOpen = isOpen && !isLocked
const current = state?.steps?.[state?.currentIndex ?? 0] const current = state?.steps?.[state?.currentIndex ?? 0]
const whoIsUp = current?.teamId const whoIsUp = current?.teamId
@ -162,8 +143,7 @@ export default function MapVoteBanner({
const isLeaderB = !!session?.user?.steamId && match.teamB?.leader?.steamId === session?.user?.steamId const isLeaderB = !!session?.user?.steamId && match.teamB?.leader?.steamId === session?.user?.steamId
const isAdmin = !!session?.user?.isAdmin const isAdmin = !!session?.user?.isAdmin
const iCanAct = Boolean( const iCanAct = Boolean(
isOpen && isVotingOpen &&
!state?.locked &&
current?.teamId && current?.teamId &&
(isAdmin || (isAdmin ||
(current.teamId === match.teamA?.id && isLeaderA) || (current.teamId === match.teamA?.id && isLeaderA) ||
@ -172,12 +152,24 @@ export default function MapVoteBanner({
const gotoFullPage = () => router.push(`/match-details/${match.id}/vote`) const gotoFullPage = () => router.push(`/match-details/${match.id}/vote`)
const cardClasses = // Farblogik: locked → grün, offen → gelb, noch geschlossen → neutral
'group relative overflow-hidden rounded-xl border bg-white/90 dark:bg-neutral-800/90 ' + const ringClass = isLocked
'dark:border-neutral-700 shadow-sm cursor-pointer focus:outline-none transition ' + ? 'ring-1 ring-green-500/15 hover:ring-green-500/30 hover:shadow-lg'
(isOpen : isVotingOpen
? 'ring-1 ring-green-500/15 hover:ring-green-500/30 hover:shadow-lg' ? 'ring-1 ring-yellow-500/20 hover:ring-yellow-500/35 hover:shadow-lg'
: 'ring-1 ring-neutral-500/10 hover:ring-neutral-500/20 hover:shadow-md') : 'ring-1 ring-neutral-500/10 hover:ring-neutral-500/20 hover:shadow-md'
const bubbleClass = isLocked
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-200'
: isVotingOpen
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-100'
: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-700/40 dark:text-neutral-200'
const gradientClass = isLocked
? 'mapVoteGradient--green'
: isVotingOpen
? 'mapVoteGradient--yellow'
: 'mapVoteGradient--none'
return ( return (
<div <div
@ -185,41 +177,35 @@ export default function MapVoteBanner({
tabIndex={0} tabIndex={0}
onClick={gotoFullPage} onClick={gotoFullPage}
onKeyDown={(e) => e.key === 'Enter' && gotoFullPage()} onKeyDown={(e) => e.key === 'Enter' && gotoFullPage()}
className={cardClasses} className={`group relative overflow-hidden rounded-xl border bg-white/90 dark:bg-neutral-800/90 dark:border-neutral-700 shadow-sm cursor-pointer focus:outline-none transition ${ringClass}`}
aria-label="Map-Vote öffnen" aria-label="Map-Vote öffnen"
> >
{isOpen && ( {(isVotingOpen || isLocked) && (
<> <>
<div aria-hidden className="absolute inset-0 z-0 pointer-events-none mapVoteGradient" /> <div aria-hidden className={`absolute inset-0 z-0 pointer-events-none ${gradientClass}`} />
<span aria-hidden className="shine pointer-events-none" /> <span aria-hidden className="shine pointer-events-none" />
</> </>
)} )}
<div className="relative z-[1] px-4 py-3 flex items-center justify-between gap-3"> <div className="relative z-[1] px-4 py-3 flex items-center justify-between gap-3">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<div className="shrink-0 w-9 h-9 rounded-full grid place-items-center bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-200 transition-transform group-hover:scale-[1.03] group-hover:translate-x-[1px]"> <div className={`shrink-0 w-9 h-9 rounded-full grid place-items-center transition-transform group-hover:scale-[1.03] group-hover:translate-x-[1px] ${bubbleClass}`}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
<path d="M15 4.5 9 7.5l-6-3v15l6 3 6-3 6 3v-15l-6-3Zm-6 16.5-4-2V6l4 2v13Zm2-13 4-2v13l-4 2V8Z"/> <path d="M15 4.5 9 7.5l-6-3v15l6 3 6-3 6 3v-15l-6-3Zm-6 16.5-4-2V6l4 2v13Zm2-13 4-2v13l-4 2V8Z"/>
</svg> </svg>
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<div className="font-medium text-gray-900 dark:text-neutral-100"> <div className="font-medium text-gray-900 dark:text-neutral-100">Map-Vote</div>
Map-Vote
</div>
<div className="text-xs text-gray-600 dark:text-neutral-400 truncate"> <div className="text-xs text-gray-600 dark:text-neutral-400 truncate">
Modus: BO{match.bestOf ?? state?.bestOf ?? 3} Modus: BO{match.bestOf ?? state?.bestOf ?? 3}
{state?.locked {state?.locked
? ' • Auswahl fixiert' ? ' • Auswahl fixiert'
: isOpen : isVotingOpen
? (whoIsUp ? ` • am Zug: ${whoIsUp}` : ' • läuft') ? (whoIsUp ? ` • am Zug: ${whoIsUp}` : ' • läuft')
: ` • startet ${formatLead(leadMinutes)} vor Matchbeginn`} : ` • startet ${formatLead(leadMinutes)} vor Matchbeginn`}
</div> </div>
{error && ( {error && <div className="text-xs text-red-600 dark:text-red-400 mt-0.5">{error}</div>}
<div className="text-xs text-red-600 dark:text-red-400 mt-0.5">
{error}
</div>
)}
</div> </div>
</div> </div>
@ -228,12 +214,11 @@ export default function MapVoteBanner({
<span className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200"> <span className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200">
Voting abgeschlossen Voting abgeschlossen
</span> </span>
) : isOpen ? ( ) : isVotingOpen ? (
<span className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200"> <span className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-100">
{iCanAct ? 'Jetzt wählen' : 'Map-Vote offen'} {iCanAct ? 'Jetzt wählen' : 'Map-Vote offen'}
</span> </span>
) : ( ) : (
// 🔑 Hydration-safe: vor dem Mount nur ein Placeholder rendern
<span <span
className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-100" className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-100"
suppressHydrationWarning suppressHydrationWarning
@ -245,63 +230,43 @@ export default function MapVoteBanner({
</div> </div>
<style jsx>{` <style jsx>{`
/* Hintergrund-Schimmer (läuft permanent) */ @keyframes slide-x { from { background-position-x: 0% } to { background-position-x: 200% } }
@keyframes slide-x {
from { background-position-x: 0%; } .mapVoteGradient--green {
to { background-position-x: 200%; } background-image: repeating-linear-gradient(90deg, rgba(16,168,54,0.20) 0%, rgba(16,168,54,0.04) 50%, rgba(16,168,54,0.20) 100%);
background-size: 200% 100%; background-repeat: repeat-x; animation: slide-x 6s linear infinite;
} }
.mapVoteGradient { :global(.dark) .mapVoteGradient--green {
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(90deg, rgba(16,168,54,0.28) 0%, rgba(16,168,54,0.08) 50%, rgba(16,168,54,0.28) 100%);
90deg,
rgba(16,168,54,0.20) 0%,
rgba(16,168,54,0.04) 50%,
rgba(16,168,54,0.20) 100%
);
background-size: 200% 100%;
background-repeat: repeat-x;
animation: slide-x 6s linear infinite;
}
:global(.dark) .mapVoteGradient {
background-image: repeating-linear-gradient(
90deg,
rgba(16,168,54,0.28) 0%,
rgba(16,168,54,0.08) 50%,
rgba(16,168,54,0.28) 100%
);
} }
/* Shine-Sweep nur auf Hover */ .mapVoteGradient--yellow {
background-image: repeating-linear-gradient(90deg, rgba(234,179,8,0.24) 0%, rgba(234,179,8,0.08) 50%, rgba(234,179,8,0.24) 100%);
background-size: 200% 100%; background-repeat: repeat-x; animation: slide-x 6s linear infinite;
}
:global(.dark) .mapVoteGradient--yellow {
background-image: repeating-linear-gradient(90deg, rgba(234,179,8,0.35) 0%, rgba(234,179,8,0.12) 50%, rgba(234,179,8,0.35) 100%);
}
.mapVoteGradient--none { background: transparent }
@keyframes shine { @keyframes shine {
0% { transform: translateX(-120%) skewX(-20deg); opacity: 0; } 0% { transform: translateX(-120%) skewX(-20deg); opacity: 0 }
10% { opacity: .7; } 10% { opacity: .7 }
27% { transform: translateX(120%) skewX(-20deg); opacity: 0; } 27% { transform: translateX(120%) skewX(-20deg); opacity: 0 }
100% { transform: translateX(120%) skewX(-20deg); opacity: 0; } 100% { transform: translateX(120%) skewX(-20deg); opacity: 0 }
}
.shine {
position: absolute;
inset: 0;
} }
.shine { position: absolute; inset: 0 }
.shine::before { .shine::before {
content: ""; content: ""; position: absolute; top: -25%; bottom: -25%; left: -20%; width: 35%;
position: absolute; pointer-events: none; background: linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
top: -25%; filter: blur(2px); opacity: 0; transform: translateX(-120%) skewX(-20deg); transition: opacity .2s;
bottom: -25%;
left: -20%;
width: 35%;
pointer-events: none;
background: linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
filter: blur(2px);
opacity: 0;
transform: translateX(-120%) skewX(-20deg);
transition: opacity .2s;
}
:global(.group:hover) .shine::before {
animation: shine 3.8s ease-out infinite;
} }
:global(.group:hover) .shine::before { animation: shine 3.8s ease-out infinite }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.mapVoteGradient { animation: none; } .mapVoteGradient--green, .mapVoteGradient--yellow { animation: none }
.shine::before { animation: none !important; transform: none !important; opacity: 0 !important; } .shine::before { animation: none !important; transform: none !important; opacity: 0 !important }
} }
`}</style> `}</style>
</div> </div>

View File

@ -1,4 +1,4 @@
// src/app/components/MapVoteProfileCard.tsx // /src/app/components/MapVoteProfileCard.tsx
'use client' 'use client'
import PremierRankBadge from './PremierRankBadge' import PremierRankBadge from './PremierRankBadge'

View File

@ -1,4 +1,4 @@
// /app/components/MatchDetails.tsx // /src/app/components/MatchDetails.tsx
'use client' 'use client'
import { useState, useEffect, useMemo, useRef } from 'react' import { useState, useEffect, useMemo, useRef } from 'react'
@ -431,13 +431,15 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
</div> </div>
{/* MapVote-Banner erhält die aktuell berechneten (SSE-konformen) Werte */} {/* MapVote-Banner erhält die aktuell berechneten (SSE-konformen) Werte */}
<MapVoteBanner {(match.matchType === 'community' &&
match={match} <MapVoteBanner
initialNow={initialNow} match={match}
matchBaseTs={matchBaseTs} initialNow={initialNow}
sseOpensAtTs={sseOpensAtTs} matchBaseTs={matchBaseTs}
sseLeadMinutes={sseLeadMinutes} sseOpensAtTs={sseOpensAtTs}
/> sseLeadMinutes={sseLeadMinutes}
/>
)}
{/* ───────── Team-Blöcke ───────── */} {/* ───────── Team-Blöcke ───────── */}
<div className="border-t pt-4 mt-4 space-y-10"> <div className="border-t pt-4 mt-4 space-y-10">

View File

@ -6,6 +6,7 @@ import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useSSEStore } from '@/app/lib/useSSEStore' import { useSSEStore } from '@/app/lib/useSSEStore'
import { NOTIFICATION_EVENTS, isSseEventType } from '../lib/sseEvents' import { NOTIFICATION_EVENTS, isSseEventType } from '../lib/sseEvents'
import { useUiChromeStore } from '@/app/lib/useUiChromeStore'
type Notification = { type Notification = {
id: string id: string
@ -36,12 +37,16 @@ export default function NotificationBell() {
const router = useRouter() const router = useRouter()
const { lastEvent } = useSSEStore() // nur konsumieren, nicht connecten const { lastEvent } = useSSEStore() // nur konsumieren, nicht connecten
const bellRef = useRef<HTMLButtonElement | null>(null); const bellRef = useRef<HTMLButtonElement | null>(null);
const telemetryBannerPx = useUiChromeStore(s => s.telemetryBannerPx)
const [notifications, setNotifications] = useState<Notification[]>([]) const [notifications, setNotifications] = useState<Notification[]>([])
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [previewText, setPreviewText] = useState<string | null>(null) const [previewText, setPreviewText] = useState<string | null>(null)
const [showPreview, setShowPreview] = useState(false) const [showPreview, setShowPreview] = useState(false)
const [animateBell, setAnimateBell] = useState(false) const [animateBell, setAnimateBell] = useState(false)
const baseBottom = 24 // px, entspricht bottom-6
const bottomPx = baseBottom + (telemetryBannerPx || 0)
useEffect(() => { useEffect(() => {
if (!lastEvent) return if (!lastEvent) return
@ -79,7 +84,7 @@ export default function NotificationBell() {
if (!steamId) return if (!steamId) return
;(async () => { ;(async () => {
try { try {
const res = await fetch('/api/notifications/user') const res = await fetch('/api/notifications')
if (!res.ok) throw new Error('Fehler beim Laden') if (!res.ok) throw new Error('Fehler beim Laden')
const data = await res.json() const data = await res.json()
const loaded: Notification[] = data.notifications.map((n: any) => ({ const loaded: Notification[] = data.notifications.map((n: any) => ({
@ -257,7 +262,10 @@ export default function NotificationBell() {
// 4) Render // 4) Render
return ( return (
<div className="fixed bottom-6 right-6 z-50"> <div
className="fixed right-6 z-50"
style={{ bottom: bottomPx }}
>
<button <button
ref={bellRef} ref={bellRef}
type="button" type="button"

View File

@ -85,18 +85,18 @@ export default function ReadyOverlayHost() {
// Events: 'match-ready' & 'map-vote-updated' // Events: 'match-ready' & 'map-vote-updated'
useEffect(() => { useEffect(() => {
if (!lastEvent || !mySteamId) return if (!lastEvent || !mySteamId) return
const evt = (lastEvent as any).payload ?? lastEvent // ⬅️ robust gegen beide Formen
if (lastEvent.type === 'match-ready') { if (lastEvent.type === 'match-ready') {
(async () => { (async () => {
const m: string | undefined = lastEvent.payload?.matchId const m: string | undefined = evt?.matchId
const participants: string[] = lastEvent.payload?.participants ?? [] const participants: string[] = evt?.participants ?? []
if (!m || !participants.includes(mySteamId) || isAccepted(m)) return if (!m || !participants.includes(mySteamId) || isAccepted(m)) return
// ⬇️ Roster persistent speichern (für Reconnect-Banner) setRoster(participants) // ✅ wird jetzt sicher gesetzt
setRoster(participants)
const label = lastEvent.payload?.firstMap?.label ?? '?' const label = evt?.firstMap?.label ?? '?'
const bg = lastEvent.payload?.firstMap?.bg ?? '/assets/img/maps/cs2.webp' const bg = evt?.firstMap?.bg ?? '/assets/img/maps/cs2.webp'
const connectHref = const connectHref =
(await getConnectHref(m)) || (await getConnectHref(m)) ||
@ -119,12 +119,11 @@ export default function ReadyOverlayHost() {
if (lastEvent.type === 'map-vote-updated') { if (lastEvent.type === 'map-vote-updated') {
(async () => { (async () => {
const summary = deriveReadySummary(lastEvent.payload) const summary = deriveReadySummary(evt) // evt statt lastEvent.payload
if (!summary) return if (!summary) return
const { matchId: m, firstMap, participants } = summary const { matchId: m, firstMap, participants } = summary
if (!participants.includes(mySteamId) || isAccepted(m)) return if (!participants.includes(mySteamId) || isAccepted(m)) return
// ⬇️ Roster persistent speichern
setRoster(participants) setRoster(participants)
const connectHref = const connectHref =

View File

@ -182,35 +182,6 @@ export default function Sidebar() {
Spielplan Spielplan
</Button> </Button>
</li> </li>
{/* Radar */}
<li>
<Button
onClick={() => { router.push('/radar'); setIsOpen(false) }}
size="sm"
variant="link"
className={`${navBtnBase} ${isActive('/radar') ? activeClasses : idleClasses}`}
>
<svg
className="size-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
{/* äußerer Kreis */}
<circle cx="12" cy="12" r="10" />
{/* Sweep-Linie */}
<line x1="12" y1="12" x2="21" y2="12" />
{/* Zusatz-Ringe */}
<circle cx="12" cy="12" r="6" strokeDasharray="4 4" />
<circle cx="12" cy="12" r="3" />
</svg>
Radar
</Button>
</li>
</ul> </ul>
</nav> </nav>

View File

@ -68,11 +68,11 @@ export default function SidebarFooter() {
}` }`
return ( return (
<div className="relative w-full"> <div className="relative w-full min-h-[65px]">
{/* Kopf / Toggle */} {/* Kopf / Toggle */}
<button <button
onClick={() => setIsOpen(v => !v)} onClick={() => setIsOpen(v => !v)}
className={`w-full inline-flex items-center gap-x-2 px-4 py-3 text-sm text-left text-gray-800 transition-all duration-100 className={`w-full min-h-[65px] inline-flex items-center gap-x-2 px-4 py-3 text-sm text-left text-gray-800 transition-all duration-100
${isOpen ? 'bg-gray-100 dark:bg-neutral-700' : 'hover:bg-gray-100 dark:hover:bg-neutral-700'} ${isOpen ? 'bg-gray-100 dark:bg-neutral-700' : 'hover:bg-gray-100 dark:hover:bg-neutral-700'}
`} `}
> >

View File

@ -1,6 +1,7 @@
// /src/app/components/TeamCard.tsx
'use client' 'use client'
import { useState } from 'react' import { useState, useMemo } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import Button from './Button' import Button from './Button'
import TeamPremierRankBadge from './TeamPremierRankBadge' import TeamPremierRankBadge from './TeamPremierRankBadge'
@ -12,6 +13,9 @@ type Props = {
invitationId?: string invitationId?: string
onUpdateInvitation: (teamId: string, newValue: string | null | 'pending') => void onUpdateInvitation: (teamId: string, newValue: string | null | 'pending') => void
adminMode?: boolean adminMode?: boolean
/** Vom Page-Container gesetzt: ob der Nutzer grundsätzlich Beitritte anfragen darf
* (false, wenn /api/user ein team liefert). Default: true (abwärtskompatibel). */
canRequestJoin?: boolean
} }
export default function TeamCard({ export default function TeamCard({
@ -20,15 +24,29 @@ export default function TeamCard({
invitationId, invitationId,
onUpdateInvitation, onUpdateInvitation,
adminMode = false, adminMode = false,
canRequestJoin = true,
}: Props) { }: Props) {
const router = useRouter() const router = useRouter()
const [joining, setJoining] = useState(false) const [joining, setJoining] = useState(false)
const isRequested = Boolean(invitationId) const isRequested = Boolean(invitationId)
const isDisabled = joining || currentUserSteamId === team.leader?.steamId
// Bin ich bereits in DIESEM Team (Leader, aktiv oder inaktiv)?
const isMemberOfThisTeam = useMemo(() => {
const inActive = (team.activePlayers ?? []).some(p => String(p.steamId) === String(currentUserSteamId))
const inInactive = (team.inactivePlayers ?? []).some(p => String(p.steamId) === String(currentUserSteamId))
const isLeader = team.leader?.steamId && String(team.leader.steamId) === String(currentUserSteamId)
return Boolean(inActive || inInactive || isLeader)
}, [team, currentUserSteamId])
// Button sperren, wenn:
// - gerade Request läuft
// - bereits Mitglied dieses Teams
// - global keine Join-Anfragen erlaubt (User hat bereits ein Team)
const isDisabled = joining || isMemberOfThisTeam || !canRequestJoin
const handleClick = async () => { const handleClick = async () => {
if (joining) return if (joining || isDisabled) return
setJoining(true) setJoining(true)
try { try {
if (isRequested) { if (isRequested) {
@ -55,6 +73,27 @@ export default function TeamCard({
const targetHref = adminMode ? `/admin/teams/${team.id}` : `/team/${team.id}` const targetHref = adminMode ? `/admin/teams/${team.id}` : `/team/${team.id}`
// Label & Farbe abhängig vom Status
const buttonLabel = joining
? (
<>
<span
className="animate-spin inline-block size-4 border-[3px] border-current border-t-transparent rounded-full mr-1"
role="status"
aria-label="loading"
/>
Lädt
</>
)
: (!canRequestJoin || isMemberOfThisTeam)
? 'Beitritt nicht möglich'
: isRequested
? 'Angefragt (zurückziehen)'
: 'Beitritt anfragen'
const buttonColor =
isDisabled ? 'gray' : (isRequested ? 'gray' : 'blue')
return ( return (
<div <div
role="button" role="button"
@ -96,27 +135,16 @@ export default function TeamCard({
Verwalten Verwalten
</Button> </Button>
) : ( ) : (
// 👉 Button immer zeigen falls nicht möglich: disabled + anderes Label
<Button <Button
title={isRequested ? 'Angefragt (zurückziehen)' : 'Beitritt anfragen'} title={typeof buttonLabel === 'string' ? buttonLabel : undefined}
size="sm" size="sm"
color={isRequested ? 'gray' : 'blue'} color={buttonColor as any}
disabled={isDisabled} disabled={isDisabled}
onClick={e => { e.stopPropagation(); handleClick() }} onClick={e => { e.stopPropagation(); handleClick() }}
aria-disabled={isDisabled ? 'true' : undefined}
> >
{joining ? ( {buttonLabel}
<>
<span
className="animate-spin inline-block size-4 border-[3px] border-current border-t-transparent rounded-full mr-1"
role="status"
aria-label="loading"
/>
Lädt
</>
) : isRequested ? (
'Angefragt'
) : (
'Beitritt anfragen'
)}
</Button> </Button>
)} )}
</div> </div>

View File

@ -1,4 +1,4 @@
// /app/components/TeamCardComponent.tsx // /src/app/components/TeamCardComponent.tsx
'use client' 'use client'

View File

@ -0,0 +1,279 @@
// src/app/components/TelemetryBanner.tsx
'use client'
import React, { useEffect, useRef } from 'react'
import Link from 'next/link'
import Button from '@/app/components/Button'
import { useUiChromeStore } from '@/app/lib/useUiChromeStore'
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
export type TelemetryBannerVariant = 'connected' | 'disconnected'
type Props = {
variant: TelemetryBannerVariant
visible: boolean
zIndex?: number
// gemeinsam
connectedCount: number
totalExpected: number
connectUri: string
onReconnect: () => void
// neu: zum harten Trennen (X-Button)
onDisconnect?: () => void
// nur für "connected"
serverLabel?: string
mapKey?: string
mapLabel?: string
phase?: string
score?: string
// nur für "disconnected"
missingCount?: number
// optional: wenn im Dock unter Main gerendert
inline?: boolean
}
/* ---------- helpers ---------- */
function hashStr(s: string): number {
let h = 5381
for (let i = 0; i < s.length; i++) h = ((h << 5) + h) + s.charCodeAt(i)
return h | 0
}
function pickMapImageFromOptions(mapKey?: string): string | null {
if (!mapKey) return null
const opt = MAP_OPTIONS.find(o => o.key.toLowerCase() === mapKey.toLowerCase())
if (!opt || !opt.images?.length) return null
const idx = Math.abs(hashStr(mapKey)) % opt.images.length
return opt.images[idx] ?? null
}
function pickMapIcon(mapKey?: string): string | null {
if (!mapKey) return null
const opt = MAP_OPTIONS.find(o => o.key.toLowerCase() === mapKey.toLowerCase())
return opt?.icon ?? null
}
/* ---------- component ---------- */
export default function TelemetryBanner({
variant,
visible,
zIndex = 9999,
connectedCount,
totalExpected,
connectUri,
onReconnect,
onDisconnect,
serverLabel,
mapKey,
mapLabel,
phase,
score,
missingCount,
inline = false,
}: Props) {
const ref = useRef<HTMLDivElement | null>(null)
const setBannerPx = useUiChromeStore(s => s.setTelemetryBannerPx)
// ▼ Phase normalisieren und Sichtbarkeit nur erlauben, wenn nicht "unknown"
const phaseStr = String(phase ?? 'unknown').toLowerCase()
const show = visible && phaseStr !== 'unknown'
useEffect(() => {
if (!show) { setBannerPx(0); return }
const el = ref.current
if (!el) return
const report = () => setBannerPx(el.getBoundingClientRect().height)
report()
const ro = new ResizeObserver(report)
ro.observe(el)
return () => { ro.disconnect(); setBannerPx(0) }
}, [show, setBannerPx])
// Ableitungen vor dem Guard
const outerBase = inline ? '' : 'fixed inset-x-0 bottom-0'
const outerStyle = inline ? {} : { zIndex }
const wrapperClass =
variant === 'connected'
? 'bg-emerald-700/95 text-white ring-1 ring-black/10'
: 'bg-amber-700/95 text-white ring-1 ring-black/10'
const bgUrl = pickMapImageFromOptions(mapKey)
const mapIconConnected = pickMapIcon(mapKey)
const iconUrl = variant === 'connected' ? (mapIconConnected ?? '') : '/assets/img/icons/ui/disconnect.svg'
const prettyMap = mapLabel ?? mapKey ?? '—'
const prettyPhase = phaseStr || 'unknown' // nutzt die normalisierte Phase
const prettyScore = score ?? ' : '
const handleFocusGame = (e: React.MouseEvent) => {
e.preventDefault()
try { window.location.href = 'steam://rungameid/730' } catch {}
}
// ▼ nichts rendern, wenn Phase unbekannt oder sichtbar=false
if (!show) return null
return (
<div className={`${outerBase} h-full w-full`} style={outerStyle} ref={ref}>
<div className={`relative overflow-hidden h-full shadow-lg ${wrapperClass} transition duration-300 ease-in-out`}>
{/* Subtiler Map-Hintergrund */}
{bgUrl && (
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage: `url(${bgUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
opacity: 0.5,
filter: 'blur(2px)',
transform: 'scale(1.02)',
}}
/>
)}
{/* Lesbarkeits-Gradient */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
background: 'linear-gradient(180deg, rgba(0,0,0,0.20) 0%, rgba(0,0,0,0.10) 40%, rgba(0,0,0,0.25) 100%)',
}}
/>
{/* Inhalt */}
<div className="relative h-full p-3 flex items-center gap-3">
{/* Icon links */}
{iconUrl ? (
<div className="shrink-0 relative z-[1]">
<div className="h-9 w-9 rounded-md bg-black/15 flex items-center justify-center ring-1 ring-black/20">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={iconUrl}
alt={variant === 'connected' ? (prettyMap || 'Map') : 'Disconnected'}
className="h-6 w-6 object-contain"
loading="eager"
decoding="async"
/>
</div>
</div>
) : null}
<div className="flex-1 min-w-0">
{variant === 'connected' ? (
<>
<div className="text-sm font-semibold">
Verbunden mit {serverLabel ?? 'CS2-Server'}
</div>
<div className="text-xs opacity-90 mt-0.5 flex flex-wrap gap-x-3 gap-y-1">
<span>Map: <span className="font-semibold">{prettyMap}</span></span>
<span>Phase: <span className="font-semibold">{prettyPhase}</span></span>
<span>Score: <span className="font-semibold">{prettyScore}</span></span>
<span>Spieler verbunden: <span className="font-semibold">{connectedCount}</span> / {totalExpected}</span>
</div>
</>
) : (
<>
<div className="text-sm font-semibold">Verbindung getrennt</div>
<div className="text-xs opacity-90 mt-0.5 flex flex-wrap gap-x-3 gap-y-1">
<span>Map: <span className="font-semibold">{prettyMap}</span></span>
<span>Phase: <span className="font-semibold">{prettyPhase}</span></span>
<span>Score: <span className="font-semibold">{prettyScore}</span></span>
<span>Spieler verbunden: <span className="font-semibold">{connectedCount}</span> / {totalExpected}</span>
</div>
</>
)}
</div>
{/* Buttons rechts */}
<div className="relative z-[1] flex items-center gap-2">
{/* Radar */}
<Link href="/radar" className="inline-flex">
<Button
variant="white"
color="transparent"
size="md"
className="bg-white/10 dark:bg-white/10 border border-white/20 hover:bg-white/15 dark:hover:bg-white/15"
title={undefined}
>
<span className="relative mr-2 h-4 w-4 rounded-full overflow-hidden ring-1 ring-black/20 bg-black/20">
{/* zentraler Punkt */}
<span className="absolute inset-0 flex items-center justify-center">
<span className="h-1.5 w-1.5 rounded-full bg-white/90 shadow-[0_0_6px_rgba(255,255,255,0.8)]" />
</span>
{/* expandierende Ringe */}
<span className="absolute inset-0 rounded-full border border-white/50 opacity-70 animate-ping-slow" />
<span className="absolute inset-0 rounded-full border border-white/30 opacity-50 animate-ping-slower" />
<span className="absolute inset-0 rounded-full border border-white/20 opacity-30 animate-ping-slowest" />
{/* rotierender Sweep */}
<span className="absolute inset-0 rounded-full overflow-hidden">
<span className="absolute left-1/2 top-1/2 origin-left -translate-y-1/2 h-[1.2px] w-full bg-gradient-to-r from-white/70 via-white/30 to-transparent animate-sweep" />
</span>
</span>
Radar
</Button>
</Link>
{/* Spiel öffnen / Neu verbinden */}
{variant === 'connected' ? (
<Button
color="green"
variant="solid"
size="md"
onClick={(e) => {
e.preventDefault()
try { window.location.href = 'steam://rungameid/730' } catch {}
}}
title="Spiel öffnen"
/>
) : (
<Button
color="green"
variant="solid"
size="md"
onClick={() => onReconnect()}
title="Neu verbinden"
/>
)}
{/* „X“ Disconnect ganz rechts */}
<Button
color="transparent"
variant="ghost"
size="md"
textSize="3xl" // darf bleiben, beeinflusst SVG nicht
className="h-9 w-9 aspect-square !px-0 grid place-items-center"
onClick={() => onDisconnect?.()}
aria-label="Verbindung trennen"
title={undefined}
>
<svg
viewBox="0 0 24 24"
className="h-6 w-6"
aria-hidden="true"
>
<path d="M6 6L18 18M18 6L6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</Button>
</div>
</div>
{/* CSS für Radar-Ping & Sweep */}
<style jsx>{`
@keyframes ringPing {
0% { transform: scale(0.4); opacity: 0.85; }
70% { transform: scale(1.05); opacity: 0.15; }
100% { transform: scale(1.2); opacity: 0; }
}
@keyframes sweepRotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.animate-ping-slow { animation: ringPing 2.2s ease-out infinite; }
.animate-ping-slower { animation: ringPing 3.0s ease-out infinite; animation-delay: .6s; }
.animate-ping-slowest { animation: ringPing 4.0s ease-out infinite; animation-delay: 1.2s; }
.animate-sweep { animation: sweepRotate 2.8s linear infinite; }
`}</style>
</div>
</div>
)
}

View File

@ -1,12 +1,14 @@
'use client' 'use client'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useSession } from 'next-auth/react'
import { useReadyOverlayStore } from '@/app/lib/useReadyOverlayStore'
import { usePresenceStore } from '@/app/lib/usePresenceStore' import { usePresenceStore } from '@/app/lib/usePresenceStore'
import { useTelemetryStore } from '@/app/lib/useTelemetryStore' import { useTelemetryStore } from '@/app/lib/useTelemetryStore'
import { useMatchRosterStore } from '@/app/lib/useMatchRosterStore' import { useMatchRosterStore } from '@/app/lib/useMatchRosterStore'
import { useSession } from 'next-auth/react' import TelemetryBanner from './TelemetryBanner'
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
type Status = 'idle'|'connecting'|'open'|'closed'|'error'
function makeWsUrl(host?: string, port?: string, path?: string, scheme?: string) { function makeWsUrl(host?: string, port?: string, path?: string, scheme?: string) {
const h = (host ?? '').trim() || '127.0.0.1' const h = (host ?? '').trim() || '127.0.0.1'
@ -16,51 +18,90 @@ function makeWsUrl(host?: string, port?: string, path?: string, scheme?: string)
const pageHttps = typeof window !== 'undefined' && window.location.protocol === 'https:' const pageHttps = typeof window !== 'undefined' && window.location.protocol === 'https:'
const useWss = sch === 'wss' || (sch !== 'ws' && (p === '443' || pageHttps)) const useWss = sch === 'wss' || (sch !== 'ws' && (p === '443' || pageHttps))
const proto = useWss ? 'wss' : 'ws' const proto = useWss ? 'wss' : 'ws'
const portPart = (p === '80' || p === '443') ? '' : `:${p}` const portPart = p === '80' || p === '443' ? '' : `:${p}`
return `${proto}://${h}${portPart}${pa}` return `${proto}://${h}${portPart}${pa}`
} }
const sidOf = (p: any) => String(p?.steamId ?? p?.steam_id ?? p?.id ?? '')
const toSet = (arr: Iterable<string>) => new Set(Array.from(arr).map(String))
function parseServerLabel(uri: string | null | undefined): string {
if (!uri) return 'CS2-Server'
const m = uri.match(/steam:\/\/connect\/([^/]+)/i)
if (m && m[1]) return m[1].split('/')[0] || 'CS2-Server'
try {
const u = new URL(uri)
return u.host || 'CS2-Server'
} catch {
return uri ?? 'CS2-Server'
}
}
function labelForMap(key?: string | null): string {
if (!key) return '—'
const k = String(key).toLowerCase()
const opt = MAP_OPTIONS.find(o => o.key.toLowerCase() === k)
if (opt?.label) return opt.label
// Fallback: "de_dust2" -> "Dust 2"
let s = k.replace(/^(de|cs)_/, '').replace(/_/g, ' ').replace(/(\d)/g, ' $1').replace(/\s+/g, ' ').trim()
s = s.split(' ').map(w => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(' ')
return s
}
export default function TelemetrySocket() { export default function TelemetrySocket() {
const url = useMemo( const url = useMemo(
() => makeWsUrl( () =>
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_HOST, makeWsUrl(
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PORT, process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_HOST,
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PATH, process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PORT,
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_SCHEME process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PATH,
), process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_SCHEME
),
[] []
) )
const { data: session } = useSession() const { data: session } = useSession()
const mySteamId = (session?.user as any)?.steamId ?? null const mySteamId = (session?.user as any)?.steamId ?? null
const setSnapshot = usePresenceStore(s => s.setSnapshot) // overlay control
const setJoin = usePresenceStore(s => s.setJoin) const hideOverlay = useReadyOverlayStore((s) => s.hide)
const setLeave = usePresenceStore(s => s.setLeave)
const setMapKey = useTelemetryStore(s => s.setMapKey) // presence/telemetry stores
const phase = useTelemetryStore(s => s.phase) const setSnapshot = usePresenceStore((s) => s.setSnapshot)
const setPhase = useTelemetryStore(s => s.setPhase) const setJoin = usePresenceStore((s) => s.setJoin)
const setLeave = usePresenceStore((s) => s.setLeave)
const rosterSet = useMatchRosterStore(s => s.roster) const setMapKey = useTelemetryStore((s) => s.setMapKey)
const phase = useTelemetryStore((s) => s.phase)
const setPhase = useTelemetryStore((s) => s.setPhase)
// 👇 Tracke, ob ICH gerade auf dem Server bin // roster (persisted by ReadyOverlayHost)
const [myConnected, setMyConnected] = useState(false) const rosterSet = useMatchRosterStore((s) => s.roster)
// internes Dismiss-Flag fürs Banner (pro Mount) // local telemetry state
const [dismissed, setDismissed] = useState(false) const [telemetrySet, setTelemetrySet] = useState<Set<string>>(new Set())
const [mapKeyForUi, setMapKeyForUi] = useState<string | null>(null)
const [score, setScore] = useState<{ a: number | null; b: number | null }>({ a: null, b: null })
// connectHref optional dynamisch vom Backend ziehen // connect uri + server name
const [connectHref, setConnectHref] = useState<string | null>(null) const [connectHref, setConnectHref] = useState<string | null>(null)
const [serverName, setServerName] = useState<string | null>(null)
// WS-Reconnect // ws
const aliveRef = useRef(true) const aliveRef = useRef(true)
const retryRef = useRef<number | null>(null) const retryRef = useRef<number | null>(null)
const wsRef = useRef<WebSocket | null>(null) const wsRef = useRef<WebSocket | null>(null)
// Connect-Href laden (Passwort etc. aus DB) // dock element (unter dem Main)
const [dockEl, setDockEl] = useState<HTMLElement | null>(null)
useEffect(() => { useEffect(() => {
(async () => { if (typeof window === 'undefined') return
setDockEl(document.getElementById('telemetry-banner-dock') as HTMLElement | null)
}, [])
// connect href from API
useEffect(() => {
;(async () => {
try { try {
const r = await fetch('/api/cs2/server', { cache: 'no-store' }) const r = await fetch('/api/cs2/server', { cache: 'no-store' })
if (r.ok) { if (r.ok) {
@ -71,6 +112,7 @@ export default function TelemetrySocket() {
})() })()
}, []) }, [])
// websocket connect
useEffect(() => { useEffect(() => {
aliveRef.current = true aliveRef.current = true
@ -95,40 +137,70 @@ export default function TelemetrySocket() {
try { msg = JSON.parse(String(ev.data ?? '')) } catch {} try { msg = JSON.parse(String(ev.data ?? '')) } catch {}
if (!msg) return if (!msg) return
if (msg.type === 'players' && Array.isArray(msg.players)) { // --- server name (optional)
setSnapshot(msg.players) if (msg.type === 'server' && typeof msg.name === 'string' && msg.name.trim()) {
// 👇 bin ich in der aktuellen Players-Liste? setServerName(msg.name.trim())
if (mySteamId) {
const present = msg.players.some((p: any) =>
String(p?.steamId ?? p?.steam_id ?? p?.id) === String(mySteamId)
)
setMyConnected(present)
}
} else if (msg.type === 'player_join' && msg.player) {
setJoin(msg.player)
// 👇 falls ich join:
const sid = msg.player?.steamId ?? msg.player?.steam_id ?? msg.player?.id
if (mySteamId && String(sid) === String(mySteamId)) setMyConnected(true)
} else if (msg.type === 'player_leave') {
const sid = msg.steamId ?? msg.steam_id ?? msg.id
if (sid != null) setLeave(sid)
// 👇 falls ich leave:
if (mySteamId && String(sid) === String(mySteamId)) setMyConnected(false)
} }
// Map (inkl. optionaler Phase) // --- full roster
if (msg.type === 'map' && typeof msg.name === 'string') { if (msg.type === 'players' && Array.isArray(msg.players)) {
const key = msg.name.toLowerCase() setSnapshot(msg.players)
if (process.env.NODE_ENV!=='production') console.debug('[TelemetrySocket] map:', key) const ids = msg.players.map(sidOf).filter(Boolean)
setMapKey(key) setTelemetrySet(toSet(ids))
if (typeof msg.phase === 'string') { if (mySteamId) {
setPhase(String(msg.phase).toLowerCase() as any) const present = msg.players.some((p: any) => sidOf(p) === String(mySteamId))
if (present) hideOverlay()
} }
} }
// Reine Phase-Events
// --- incremental roster
if (msg.type === 'player_join' && msg.player) {
setJoin(msg.player)
setTelemetrySet(prev => {
const next = new Set(prev)
const sid = sidOf(msg.player)
if (sid) next.add(sid)
return next
})
const sid = sidOf(msg.player)
if (mySteamId && sid && sid === String(mySteamId)) hideOverlay()
}
if (msg.type === 'player_leave') {
const sid = String(msg.steamId ?? msg.steam_id ?? msg.id ?? '')
if (sid) setLeave(sid)
setTelemetrySet(prev => {
const next = new Set(prev)
if (sid) next.delete(sid)
return next
})
}
// --- map: NUR map + optional serverName, NICHT die Phase übernehmen
if (msg.type === 'map' && typeof msg.name === 'string') {
const key = msg.name.toLowerCase()
setMapKey(key)
setMapKeyForUi(key)
if (typeof msg.serverName === 'string' && msg.serverName.trim()) {
setServerName(msg.serverName.trim())
}
}
// --- phase: AUSSCHLIESSLICHE Quelle für die Phase
if (msg.type === 'phase' && typeof msg.phase === 'string') { if (msg.type === 'phase' && typeof msg.phase === 'string') {
setPhase(String(msg.phase).toLowerCase() as any) setPhase(String(msg.phase).toLowerCase() as any)
} }
// --- score (unverändert)
if (msg.type === 'score') {
const a = Number(msg.team1 ?? msg.ct)
const b = Number(msg.team2 ?? msg.t)
setScore({ a: Number.isFinite(a) ? a : null, b: Number.isFinite(b) ? b : null })
} else if (msg.score) {
const a = Number(msg.score.team1 ?? msg.score.ct)
const b = Number(msg.score.team2 ?? msg.score.t)
setScore({ a: Number.isFinite(a) ? a : null, b: Number.isFinite(b) ? b : null })
}
} }
} }
@ -138,61 +210,87 @@ export default function TelemetrySocket() {
if (retryRef.current) window.clearTimeout(retryRef.current) if (retryRef.current) window.clearTimeout(retryRef.current)
try { wsRef.current?.close(1000, 'telemetry socket unmounted') } catch {} try { wsRef.current?.close(1000, 'telemetry socket unmounted') } catch {}
} }
}, [url, setSnapshot, setJoin, setLeave, setMapKey, setPhase, mySteamId]) }, [url, setSnapshot, setJoin, setLeave, setMapKey, setPhase, hideOverlay, mySteamId])
// Anzeige-Logik Banner: // ----- banner logic (connected + disconnected variants) with roster fallback
// - Ich bin im Roster (Match wurde geladen & ich bin Teilnehmer) const myId = mySteamId ? String(mySteamId) : null
// - Match-Phase live const roster =
// - Ich bin NICHT connected (disconnect) rosterSet instanceof Set && rosterSet.size > 0
const meInRoster = !!mySteamId && rosterSet instanceof Set && rosterSet.has(String(mySteamId)) ? rosterSet
const shouldShow = !dismissed && (phase === 'live') && meInRoster && !myConnected : (myId ? new Set<string>([myId]) : new Set<string>())
const iAmExpected = !!myId && roster.has(myId)
const iAmOnline = !!myId && telemetrySet.has(myId)
const intersectCount = (() => {
if (roster.size === 0) return 0
let n = 0
for (const sid of roster) if (telemetrySet.has(sid)) n++
return n
})()
const totalExpected = roster.size
const connectUri = const connectUri =
connectHref // ⬅️ bevorzugt API-Route (inkl. Passwort) connectHref ||
|| process.env.NEXT_PUBLIC_STEAM_CONNECT_URI process.env.NEXT_PUBLIC_STEAM_CONNECT_URI ||
|| process.env.NEXT_PUBLIC_CS2_CONNECT_URI process.env.NEXT_PUBLIC_CS2_CONNECT_URI ||
|| 'steam://rungameid/730//+retry' // Fallback 'steam://rungameid/730//+retry'
// Fallback-Label aus URI, falls kein Servername vom WS kam
const fallbackServerLabel = parseServerLabel(connectUri)
const effectiveServerLabel = (serverName && serverName.trim()) || fallbackServerLabel
const prettyPhase = phase ?? 'unknown'
const prettyScore = (score.a == null || score.b == null) ? ' : ' : `${score.a} : ${score.b}`
const prettyMapLabel = labelForMap(mapKeyForUi)
const handleReconnect = () => { const handleReconnect = () => {
try { try { window.location.href = connectUri } catch {}
window.location.href = connectUri
} catch {
// no-op
}
} }
return ( const handleDisconnect = () => {
<> // Auto-Reconnect stoppen
{/* WebSocket-Client selbst rendert nichts */} aliveRef.current = false;
{shouldShow && ( if (retryRef.current) {
<div className="fixed inset-x-0 bottom-0 z-[9999] mx-auto mb-3 max-w-3xl"> window.clearTimeout(retryRef.current);
<div className="mx-3 rounded-md bg-neutral-900/95 text-white shadow-lg ring-1 ring-black/10"> retryRef.current = null;
<div className="p-3 flex items-center gap-3"> }
<div className="flex-1">
<div className="text-sm font-semibold">Match läuft · Reconnect verfügbar</div> // WebSocket sauber schließen
<div className="text-xs opacity-80"> try { wsRef.current?.close(1000, 'user requested disconnect') } catch {}
Du bist im aufgesetzten Match eingetragen. Klicke Reconnect, um wieder zu joinen. wsRef.current = null;
</div>
</div> // Lokalen Zustand zurücksetzen (wir bleiben im "disconnected"-Banner)
<div className="flex items-center gap-2"> setTelemetrySet(new Set());
<button setServerName(null);
onClick={handleReconnect} setMapKeyForUi(null);
className="px-3 py-1.5 rounded bg-emerald-500 hover:bg-emerald-600 text-sm font-semibold" setPhase('unknown' as any);
> setScore({ a: null, b: null });
Reconnect };
</button>
<button const variant: 'connected' | 'disconnected' = iAmOnline ? 'connected' : 'disconnected'
onClick={() => setDismissed(true)} const visible = iAmExpected
className="px-2 py-1 rounded bg-neutral-700 hover:bg-neutral-600 text-xs" const zIndex = iAmOnline ? 9998 : 9999
aria-label="schließen"
> const bannerEl = (
<TelemetryBanner
</button> variant={variant}
</div> visible={visible}
</div> zIndex={zIndex}
</div> inline={!!dockEl}
</div> connectedCount={intersectCount}
)} totalExpected={totalExpected}
</> connectUri={connectUri}
onReconnect={handleReconnect}
onDisconnect={handleDisconnect}
serverLabel={effectiveServerLabel}
mapKey={mapKeyForUi ?? undefined}
mapLabel={prettyMapLabel}
phase={prettyPhase}
score={prettyScore}
missingCount={totalExpected - intersectCount}
/>
) )
return dockEl ? createPortal(bannerEl, dockEl) : bannerEl
} }

View File

@ -1,4 +1,4 @@
// src/app/components/UserAvatarWithStatus.tsx // /src/app/components/UserAvatarWithStatus.tsx
'use client' 'use client'
import { useEffect, useState, useMemo } from 'react' import { useEffect, useState, useMemo } from 'react'

View File

@ -1,4 +1,4 @@
// /app/components/UserHeader.tsx // /src/app/components/UserHeader.tsx
import { Tabs } from '@/app/components/Tabs' import { Tabs } from '@/app/components/Tabs'
import PremierRankBadge from './PremierRankBadge' import PremierRankBadge from './PremierRankBadge'

View File

@ -1,3 +1,5 @@
// /src/app/components/admin/teams/AdminTeamsView.tsx
'use client' 'use client'
import { useEffect, useState, useRef, useCallback } from 'react' import { useEffect, useState, useRef, useCallback } from 'react'
@ -25,9 +27,15 @@ export default function AdminTeamsView() {
const fetchTeams = useCallback(async () => { const fetchTeams = useCallback(async () => {
setLoading(true) setLoading(true)
try { try {
const res = await fetch('/api/teams') const res = await fetch('/api/teams', { cache: 'no-store' })
const json = await res.json() const json = await res.json()
setTeams(json.teams ?? [])
// robust parsen: Array ODER Objekt mit teams/items
const list: Team[] = Array.isArray(json)
? json
: (json?.teams ?? json?.items ?? [])
setTeams(list)
} catch (err) { } catch (err) {
console.error('[AdminTeamsView] /api/teams:', err) console.error('[AdminTeamsView] /api/teams:', err)
setTeams([]) setTeams([])

View File

@ -1,3 +1,5 @@
// /src/app/components/profile/[steamId]/matches/UserMatchesList.tsx
'use client' 'use client'
import { useEffect, useRef, useState, useCallback } from 'react' import { useEffect, useRef, useState, useCallback } from 'react'
@ -230,13 +232,47 @@ export default function UserMatchesList({ steamId }: { steamId: string }) {
> >
<Table.Cell hoverable> <Table.Cell hoverable>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<img {(() => {
src={`/assets/img/mapicons/${m.map}.webp`} const raw = m.map || ''
alt={mapInfo?.label} const normKey = raw.replace(/^de_/, '') // "de_ancient" -> "ancient"
width={32}
height={32} // 1) Versuch: exact normKey
/> // 2) Versuch: raw (mit "de_")
{mapInfo?.label} // 3) Versuch: wieder mit "de_" (falls m.map ohne Prefix kam)
const opt =
MAP_OPTIONS.find(o => o.key === normKey) ||
MAP_OPTIONS.find(o => o.key === raw) ||
MAP_OPTIONS.find(o => o.key === `de_${normKey}`) ||
null
const label =
opt?.label ||
normKey || // sinnvoller Text, falls alles fehlt
'Map'
// Icon-Quelle: erst aus MAP_OPTIONS, sonst generischer Pfad auf Basis des normKey
const iconSrc = opt?.icon || `/assets/img/mapicons/map_icon_${normKey}.svg`
// Debug (kannst du wieder entfernen)
// console.log({ raw, normKey, optKey: opt?.key, label, iconSrc })
return (
<>
<img
src={iconSrc}
alt={label}
width={32}
height={32}
className="shrink-0"
onError={(e) => {
(e.currentTarget as HTMLImageElement).src =
'/assets/img/mapicons/map_icon_lobby_mapveto.svg'
}}
/>
{label}
</>
)
})()}
</div> </div>
</Table.Cell> </Table.Cell>

View File

@ -1,4 +1,4 @@
// src/app/components/radar/StaticEffects.tsx // /src/app/components/radar/StaticEffects.tsx
'use client' 'use client'
import React from 'react' import React from 'react'

View File

@ -1,76 +1,54 @@
// /src/app/dashboard/page.tsx
'use client' 'use client'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import Modal from '@/app/components/Modal'
import Link from 'next/link'
import Button from '../components/Button'
import ComboBox from '../components/ComboBox'
export default function Dashboard() { export default function Dashboard() {
const { data: session, status } = useSession() const { data: session, status } = useSession()
const [showTeamModal, setShowTeamModal] = useState(false)
const [teams, setTeams] = useState<string[]>([]) const [teams, setTeams] = useState<string[]>([])
const [selectedTeam, setSelectedTeam] = useState('') const [selectedTeam, setSelectedTeam] = useState('')
// Teams laden (robust)
useEffect(() => { useEffect(() => {
if (status === 'authenticated' && !session?.user?.team) { let abort = false
setShowTeamModal(true)
}
}, [session, status])
useEffect(() => { async function fetchTeams() {
if (showTeamModal) {
const open = setTimeout(() => {
const modalEl = document.getElementById('hs-vertically-centered-modal')
if (modalEl && typeof window.HSOverlay?.open === 'function') {
try {
window.HSOverlay.open(modalEl)
} catch (err) {
console.error('Fehler beim Öffnen des Modals:', err)
}
}
}, 300)
return () => clearTimeout(open)
}
}, [showTeamModal])
useEffect(() => {
const fetchTeams = async () => {
try { try {
const res = await fetch('/api/teams') const res = await fetch('/api/teams', { cache: 'no-store' })
const data = await res.json() if (!res.ok) throw new Error(`HTTP ${res.status}`)
setTeams(data.teams.map((t: { teamname: string }) => t.teamname))
let json: any = null
try { json = await res.json() } catch {}
const teamsArr: any[] =
Array.isArray(json?.teams) ? json.teams :
Array.isArray(json?.data) ? json.data :
Array.isArray(json) ? json :
[]
if (!abort) {
setTeams(teamsArr.map((t) => t?.teamname ?? t?.name ?? 'Unbenannt'))
}
} catch (error) { } catch (error) {
console.error('Fehler beim Laden der Teams:', error) console.error('Fehler beim Laden der Teams:', error)
if (!abort) setTeams([])
} }
} }
fetchTeams() fetchTeams()
return () => { abort = true }
}, []) }, [])
return ( return (
<> <>
{showTeamModal && (
<Modal
title="Kein Team gefunden"
show={true}
id="no-team-modal"
closeButtonColor="blue"
hideCloseButton
>
<p className="text-sm text-gray-700 dark:text-neutral-300">
Du bist aktuell keinem Team beigetreten. Bitte tritt einem Team bei oder erstelle eines.
</p>
<Link href='/settings/team' className='center'>
<Button title='Team auswählen'></Button>
</Link>
</Modal>
)}
<h1 className="text-2xl font-bold mb-4 text-gray-800 dark:text-white"> <h1 className="text-2xl font-bold mb-4 text-gray-800 dark:text-white">
Willkommen im Dashboard! Willkommen im Dashboard!
</h1> </h1>
{/* Beispiel: Teams anzeigen (optional) */}
{/* <pre className="text-xs opacity-70">{JSON.stringify(teams, null, 2)}</pre> */}
</> </>
) )
} }

View File

@ -53,6 +53,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
{children} {children}
</div> </div>
</main> </main>
<div id="telemetry-banner-dock" className="h-full max-h-[65px]" />
</div> </div>
</div> </div>

View File

@ -35,7 +35,6 @@ export const MAP_OPTIONS: MapOption[] = [
'de_dust2_1_png.webp', 'de_dust2_1_png.webp',
'de_dust2_2_png.webp', 'de_dust2_2_png.webp',
'de_dust2_3_png.webp', 'de_dust2_3_png.webp',
'de_dust2_4_png.webp',
'de_dust2_png.webp', 'de_dust2_png.webp',
]), ]),
icon: iconFor('de_dust2'), icon: iconFor('de_dust2'),

View File

@ -1,4 +1,4 @@
// src/lib/prisma.ts // /src/lib/prisma.ts
import { PrismaClient } from '@/generated/prisma' import { PrismaClient } from '@/generated/prisma'
const globalForPrisma = globalThis as unknown as { const globalForPrisma = globalThis as unknown as {

View File

@ -1,4 +1,4 @@
// src/app/lib/signOutWithStatus.ts // /src/app/lib/signOutWithStatus.ts
'use client' 'use client'
import { signOut } from 'next-auth/react' import { signOut } from 'next-auth/react'

View File

@ -1,4 +1,4 @@
// /app/lib/sseEvents.ts // /src/app/lib/sseEvents.ts
export const SSE_EVENT_TYPES = [ export const SSE_EVENT_TYPES = [
// Kanonisch // Kanonisch

View File

@ -1,4 +1,4 @@
// /app/lib/useMatchRosterStore.ts // /src/app/lib/useMatchRosterStore.ts
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'

View File

@ -1,4 +1,4 @@
// /app/lib/useTelemetryStore.ts // /src/app/lib/useTelemetryStore.ts
import { create } from 'zustand' import { create } from 'zustand'
type TelemetryState = { type TelemetryState = {

View File

@ -0,0 +1,12 @@
'use client'
import { create } from 'zustand'
type UiChromeState = {
telemetryBannerPx: number // aktuelle Bannerhöhe in Pixel (0 wenn unsichtbar)
setTelemetryBannerPx: (px: number) => void
}
export const useUiChromeStore = create<UiChromeState>((set) => ({
telemetryBannerPx: 0,
setTelemetryBannerPx: (px) => set({ telemetryBannerPx: Math.max(0, Math.floor(px)) }),
}))

View File

@ -1,4 +1,4 @@
// /app/profile/[steamId]/layout.tsx // /src/app/profile/[steamId]/layout.tsx
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'

View File

@ -1,4 +1,4 @@
// /app/profile/[steamId]/matches/page.tsx // /src/app/profile/[steamId]/matches/page.tsx
import Card from '@/app/components/Card' import Card from '@/app/components/Card'
import UserMatchesList from '@/app/components/profile/[steamId]/matches/UserMatchesList' import UserMatchesList from '@/app/components/profile/[steamId]/matches/UserMatchesList'

View File

@ -1,4 +1,4 @@
// /app/profile/[steamId]/page.tsx // /src/app/profile/[steamId]/page.tsx
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
export default function ProfileRedirect({ params }: { params: { steamId: string } }) { export default function ProfileRedirect({ params }: { params: { steamId: string } }) {

View File

@ -1,4 +1,4 @@
// /app/profile/[steamId]/stats/page.tsx // /src/app/profile/[steamId]/stats/page.tsx
import UserProfile from '@/app/components/profile/[steamId]/stats/UserProfile' import UserProfile from '@/app/components/profile/[steamId]/stats/UserProfile'
import { MatchStats } from '@/app/types/match' import { MatchStats } from '@/app/types/match'

View File

@ -1,4 +1,4 @@
// src/app/components/radar/LiveRadar.tsx // /src/app/components/radar/LiveRadar.tsx
'use client' 'use client'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'

View File

@ -1,4 +1,4 @@
// src/app/team/[teamId]/page.tsx // /src/app/team/[teamId]/page.tsx
'use client' 'use client'
import { useEffect, useState, KeyboardEvent, MouseEvent } from 'react' import { useEffect, useState, KeyboardEvent, MouseEvent } from 'react'

View File

@ -1,4 +1,4 @@
// /app/team/page.tsx // /src/app/team/page.tsx
'use client'; 'use client';

173
src/app/teams/page.tsx Normal file
View File

@ -0,0 +1,173 @@
// /app/teams/page.tsx
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
import { useSession } from 'next-auth/react'
import Button from '@/app/components/Button'
import Modal from '@/app/components/Modal'
import Input from '@/app/components/Input'
import TeamCard from '@/app/components/TeamCard'
import LoadingSpinner from '@/app/components/LoadingSpinner'
import type { Team } from '@/app/types/team'
import Card from '../components/Card'
type TeamsResp = any
type InvitesResp = { invitations?: any[] } | any
type UserResp = { team?: any } | any
export default function TeamsPage() {
const { data: session } = useSession()
const mySteamId = (session?.user as any)?.steamId ?? ''
const [teams, setTeams] = useState<Team[]>([])
const [loading, setLoading] = useState(true)
const [invitationMap, setInvitationMap] = useState<Record<string, string>>({})
const [showCreate, setShowCreate] = useState(false)
const [newName, setNewName] = useState('')
const [saving, setSaving] = useState(false)
// NEU: Flag aus /api/user
const [userHasTeam, setUserHasTeam] = useState<boolean>(false)
const parseTeams = (data: TeamsResp): Team[] => {
if (Array.isArray(data)) return data
return (data?.items ?? data?.teams ?? []) as Team[]
}
const fetchAll = useCallback(async () => {
setLoading(true)
try {
const [tRes, iRes, uRes] = await Promise.all([
fetch('/api/teams', { cache: 'no-store' }),
fetch('/api/user/invitations', { cache: 'no-store' }),
fetch('/api/user', { cache: 'no-store' }), // ⬅️ eigene Team-Mitgliedschaft
])
const [tJson, iJson, uJson]: [TeamsResp, InvitesResp, UserResp] = await Promise.all([
tRes.json(),
iRes.json().catch(() => ({} as any)),
uRes.ok ? uRes.json() : ({} as any),
])
const nextTeams = parseTeams(tJson)
const nextMap: Record<string, string> = {}
const invites = Array.isArray(iJson) ? iJson : (iJson?.invitations ?? [])
for (const inv of invites) {
if (inv?.type === 'team-join-request' && inv?.teamId && inv?.id) {
nextMap[inv.teamId] = inv.id
}
}
setTeams(nextTeams)
setInvitationMap(nextMap)
setUserHasTeam(!!uJson?.team) // ⬅️ entscheidend
} catch (err) {
console.error('[TeamsPage] load failed:', err)
setTeams([])
setInvitationMap({})
setUserHasTeam(false)
} finally {
setLoading(false)
}
}, [])
const fetchedOnce = useRef(false)
useEffect(() => {
if (fetchedOnce.current) return
fetchedOnce.current = true
fetchAll()
}, [fetchAll])
const createTeam = async () => {
if (!newName.trim()) return
setSaving(true)
try {
const res = await fetch('/api/team/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ teamname: newName.trim() }),
})
if (!res.ok) {
const j = await res.json().catch(() => ({}))
alert(j?.message ?? 'Team konnte nicht erstellt werden.')
return
}
await fetchAll()
setShowCreate(false)
setNewName('')
} catch (e) {
console.error('[TeamsPage] createTeam:', e)
alert('Team konnte nicht erstellt werden.')
} finally {
setSaving(false)
}
}
if (loading) {
return (
<div className="text-gray-500 dark:text-gray-400">
<LoadingSpinner />
</div>
)
}
// Nur anzeigen, wenn der Spieler in KEINEM Team ist
const canRequestJoin = !userHasTeam
return (
<Card maxWidth='full'>
<div className="flex justify-between items-center mb-6">
<h2 className="text-lg font-semibold text-gray-800 dark:text-white">
Teams verwalten
</h2>
<Button color="blue" onClick={() => setShowCreate(true)}>
Neues Team erstellen
</Button>
</div>
{teams.length === 0 ? (
<div className="text-gray-500 dark:text-gray-400">
Es wurden noch keine Teams erstellt.
</div>
) : (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{teams.map((t) => (
<TeamCard
key={t.id}
team={t}
currentUserSteamId={mySteamId}
invitationId={invitationMap[t.id]}
onUpdateInvitation={(teamId, inviteId) =>
setInvitationMap((prev) => ({ ...prev, [teamId]: inviteId }))
}
canRequestJoin={canRequestJoin} // ⬅️ Button-Logik
/>
))}
</div>
)}
<Modal
id="create-team-modal"
title="Neues Team erstellen"
show={showCreate}
onClose={() => {
setShowCreate(false)
setNewName('')
}}
onSave={createTeam}
closeButtonColor="blue"
closeButtonTitle={saving ? 'Speichern …' : 'Erstellen'}
>
<Input
label="Teamname"
value={newName}
placeholder="z. B. Ironie eSports"
onChange={(e) => setNewName(e.target.value)}
/>
</Modal>
</Card>
)
}

View File

@ -1,4 +1,4 @@
// src/app/types/match.ts // /src/app/types/match.ts
import { Player, Team } from './team' import { Player, Team } from './team'

File diff suppressed because one or more lines are too long

View File

@ -186,7 +186,9 @@ exports.Prisma.MatchScalarFieldEnum = {
bestOf: 'bestOf', bestOf: 'bestOf',
matchDate: 'matchDate', matchDate: 'matchDate',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt',
cs2MatchId: 'cs2MatchId',
exportedAt: 'exportedAt'
}; };
exports.Prisma.MatchPlayerScalarFieldEnum = { exports.Prisma.MatchPlayerScalarFieldEnum = {

View File

@ -7604,6 +7604,7 @@ export namespace Prisma {
scoreB: number | null scoreB: number | null
roundCount: number | null roundCount: number | null
bestOf: number | null bestOf: number | null
cs2MatchId: number | null
} }
export type MatchSumAggregateOutputType = { export type MatchSumAggregateOutputType = {
@ -7611,6 +7612,7 @@ export namespace Prisma {
scoreB: number | null scoreB: number | null
roundCount: number | null roundCount: number | null
bestOf: number | null bestOf: number | null
cs2MatchId: number | null
} }
export type MatchMinAggregateOutputType = { export type MatchMinAggregateOutputType = {
@ -7631,6 +7633,8 @@ export namespace Prisma {
matchDate: Date | null matchDate: Date | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
cs2MatchId: number | null
exportedAt: Date | null
} }
export type MatchMaxAggregateOutputType = { export type MatchMaxAggregateOutputType = {
@ -7651,6 +7655,8 @@ export namespace Prisma {
matchDate: Date | null matchDate: Date | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
cs2MatchId: number | null
exportedAt: Date | null
} }
export type MatchCountAggregateOutputType = { export type MatchCountAggregateOutputType = {
@ -7673,6 +7679,8 @@ export namespace Prisma {
matchDate: number matchDate: number
createdAt: number createdAt: number
updatedAt: number updatedAt: number
cs2MatchId: number
exportedAt: number
_all: number _all: number
} }
@ -7682,6 +7690,7 @@ export namespace Prisma {
scoreB?: true scoreB?: true
roundCount?: true roundCount?: true
bestOf?: true bestOf?: true
cs2MatchId?: true
} }
export type MatchSumAggregateInputType = { export type MatchSumAggregateInputType = {
@ -7689,6 +7698,7 @@ export namespace Prisma {
scoreB?: true scoreB?: true
roundCount?: true roundCount?: true
bestOf?: true bestOf?: true
cs2MatchId?: true
} }
export type MatchMinAggregateInputType = { export type MatchMinAggregateInputType = {
@ -7709,6 +7719,8 @@ export namespace Prisma {
matchDate?: true matchDate?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
cs2MatchId?: true
exportedAt?: true
} }
export type MatchMaxAggregateInputType = { export type MatchMaxAggregateInputType = {
@ -7729,6 +7741,8 @@ export namespace Prisma {
matchDate?: true matchDate?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
cs2MatchId?: true
exportedAt?: true
} }
export type MatchCountAggregateInputType = { export type MatchCountAggregateInputType = {
@ -7751,6 +7765,8 @@ export namespace Prisma {
matchDate?: true matchDate?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
cs2MatchId?: true
exportedAt?: true
_all?: true _all?: true
} }
@ -7860,6 +7876,8 @@ export namespace Prisma {
matchDate: Date | null matchDate: Date | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
cs2MatchId: number | null
exportedAt: Date | null
_count: MatchCountAggregateOutputType | null _count: MatchCountAggregateOutputType | null
_avg: MatchAvgAggregateOutputType | null _avg: MatchAvgAggregateOutputType | null
_sum: MatchSumAggregateOutputType | null _sum: MatchSumAggregateOutputType | null
@ -7901,6 +7919,8 @@ export namespace Prisma {
matchDate?: boolean matchDate?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
cs2MatchId?: boolean
exportedAt?: boolean
teamA?: boolean | Match$teamAArgs<ExtArgs> teamA?: boolean | Match$teamAArgs<ExtArgs>
teamB?: boolean | Match$teamBArgs<ExtArgs> teamB?: boolean | Match$teamBArgs<ExtArgs>
teamAUsers?: boolean | Match$teamAUsersArgs<ExtArgs> teamAUsers?: boolean | Match$teamAUsersArgs<ExtArgs>
@ -7934,6 +7954,8 @@ export namespace Prisma {
matchDate?: boolean matchDate?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
cs2MatchId?: boolean
exportedAt?: boolean
teamA?: boolean | Match$teamAArgs<ExtArgs> teamA?: boolean | Match$teamAArgs<ExtArgs>
teamB?: boolean | Match$teamBArgs<ExtArgs> teamB?: boolean | Match$teamBArgs<ExtArgs>
}, ExtArgs["result"]["match"]> }, ExtArgs["result"]["match"]>
@ -7958,6 +7980,8 @@ export namespace Prisma {
matchDate?: boolean matchDate?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
cs2MatchId?: boolean
exportedAt?: boolean
teamA?: boolean | Match$teamAArgs<ExtArgs> teamA?: boolean | Match$teamAArgs<ExtArgs>
teamB?: boolean | Match$teamBArgs<ExtArgs> teamB?: boolean | Match$teamBArgs<ExtArgs>
}, ExtArgs["result"]["match"]> }, ExtArgs["result"]["match"]>
@ -7982,9 +8006,11 @@ export namespace Prisma {
matchDate?: boolean matchDate?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
cs2MatchId?: boolean
exportedAt?: boolean
} }
export type MatchOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "title" | "matchType" | "map" | "description" | "scoreA" | "scoreB" | "teamAId" | "teamBId" | "filePath" | "demoDate" | "demoData" | "roundCount" | "roundHistory" | "winnerTeam" | "bestOf" | "matchDate" | "createdAt" | "updatedAt", ExtArgs["result"]["match"]> export type MatchOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "title" | "matchType" | "map" | "description" | "scoreA" | "scoreB" | "teamAId" | "teamBId" | "filePath" | "demoDate" | "demoData" | "roundCount" | "roundHistory" | "winnerTeam" | "bestOf" | "matchDate" | "createdAt" | "updatedAt" | "cs2MatchId" | "exportedAt", ExtArgs["result"]["match"]>
export type MatchInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = { export type MatchInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
teamA?: boolean | Match$teamAArgs<ExtArgs> teamA?: boolean | Match$teamAArgs<ExtArgs>
teamB?: boolean | Match$teamBArgs<ExtArgs> teamB?: boolean | Match$teamBArgs<ExtArgs>
@ -8041,6 +8067,8 @@ export namespace Prisma {
matchDate: Date | null matchDate: Date | null
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
cs2MatchId: number | null
exportedAt: Date | null
}, ExtArgs["result"]["match"]> }, ExtArgs["result"]["match"]>
composites: {} composites: {}
} }
@ -8493,6 +8521,8 @@ export namespace Prisma {
readonly matchDate: FieldRef<"Match", 'DateTime'> readonly matchDate: FieldRef<"Match", 'DateTime'>
readonly createdAt: FieldRef<"Match", 'DateTime'> readonly createdAt: FieldRef<"Match", 'DateTime'>
readonly updatedAt: FieldRef<"Match", 'DateTime'> readonly updatedAt: FieldRef<"Match", 'DateTime'>
readonly cs2MatchId: FieldRef<"Match", 'Int'>
readonly exportedAt: FieldRef<"Match", 'DateTime'>
} }
@ -20998,7 +21028,9 @@ export namespace Prisma {
bestOf: 'bestOf', bestOf: 'bestOf',
matchDate: 'matchDate', matchDate: 'matchDate',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt',
cs2MatchId: 'cs2MatchId',
exportedAt: 'exportedAt'
}; };
export type MatchScalarFieldEnum = (typeof MatchScalarFieldEnum)[keyof typeof MatchScalarFieldEnum] export type MatchScalarFieldEnum = (typeof MatchScalarFieldEnum)[keyof typeof MatchScalarFieldEnum]
@ -21734,6 +21766,8 @@ export namespace Prisma {
matchDate?: DateTimeNullableFilter<"Match"> | Date | string | null matchDate?: DateTimeNullableFilter<"Match"> | Date | string | null
createdAt?: DateTimeFilter<"Match"> | Date | string createdAt?: DateTimeFilter<"Match"> | Date | string
updatedAt?: DateTimeFilter<"Match"> | Date | string updatedAt?: DateTimeFilter<"Match"> | Date | string
cs2MatchId?: IntNullableFilter<"Match"> | number | null
exportedAt?: DateTimeNullableFilter<"Match"> | Date | string | null
teamA?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null teamA?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
teamB?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null teamB?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
teamAUsers?: UserListRelationFilter teamAUsers?: UserListRelationFilter
@ -21766,6 +21800,8 @@ export namespace Prisma {
matchDate?: SortOrderInput | SortOrder matchDate?: SortOrderInput | SortOrder
createdAt?: SortOrder createdAt?: SortOrder
updatedAt?: SortOrder updatedAt?: SortOrder
cs2MatchId?: SortOrderInput | SortOrder
exportedAt?: SortOrderInput | SortOrder
teamA?: TeamOrderByWithRelationInput teamA?: TeamOrderByWithRelationInput
teamB?: TeamOrderByWithRelationInput teamB?: TeamOrderByWithRelationInput
teamAUsers?: UserOrderByRelationAggregateInput teamAUsers?: UserOrderByRelationAggregateInput
@ -21801,6 +21837,8 @@ export namespace Prisma {
matchDate?: DateTimeNullableFilter<"Match"> | Date | string | null matchDate?: DateTimeNullableFilter<"Match"> | Date | string | null
createdAt?: DateTimeFilter<"Match"> | Date | string createdAt?: DateTimeFilter<"Match"> | Date | string
updatedAt?: DateTimeFilter<"Match"> | Date | string updatedAt?: DateTimeFilter<"Match"> | Date | string
cs2MatchId?: IntNullableFilter<"Match"> | number | null
exportedAt?: DateTimeNullableFilter<"Match"> | Date | string | null
teamA?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null teamA?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
teamB?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null teamB?: XOR<TeamNullableScalarRelationFilter, TeamWhereInput> | null
teamAUsers?: UserListRelationFilter teamAUsers?: UserListRelationFilter
@ -21833,6 +21871,8 @@ export namespace Prisma {
matchDate?: SortOrderInput | SortOrder matchDate?: SortOrderInput | SortOrder
createdAt?: SortOrder createdAt?: SortOrder
updatedAt?: SortOrder updatedAt?: SortOrder
cs2MatchId?: SortOrderInput | SortOrder
exportedAt?: SortOrderInput | SortOrder
_count?: MatchCountOrderByAggregateInput _count?: MatchCountOrderByAggregateInput
_avg?: MatchAvgOrderByAggregateInput _avg?: MatchAvgOrderByAggregateInput
_max?: MatchMaxOrderByAggregateInput _max?: MatchMaxOrderByAggregateInput
@ -21863,6 +21903,8 @@ export namespace Prisma {
matchDate?: DateTimeNullableWithAggregatesFilter<"Match"> | Date | string | null matchDate?: DateTimeNullableWithAggregatesFilter<"Match"> | Date | string | null
createdAt?: DateTimeWithAggregatesFilter<"Match"> | Date | string createdAt?: DateTimeWithAggregatesFilter<"Match"> | Date | string
updatedAt?: DateTimeWithAggregatesFilter<"Match"> | Date | string updatedAt?: DateTimeWithAggregatesFilter<"Match"> | Date | string
cs2MatchId?: IntNullableWithAggregatesFilter<"Match"> | number | null
exportedAt?: DateTimeNullableWithAggregatesFilter<"Match"> | Date | string | null
} }
export type MatchPlayerWhereInput = { export type MatchPlayerWhereInput = {
@ -23152,6 +23194,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
@ -23184,6 +23228,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
@ -23212,6 +23258,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
@ -23244,6 +23292,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
@ -23274,6 +23324,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
} }
export type MatchUpdateManyMutationInput = { export type MatchUpdateManyMutationInput = {
@ -23294,6 +23346,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
} }
export type MatchUncheckedUpdateManyInput = { export type MatchUncheckedUpdateManyInput = {
@ -23316,6 +23370,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
} }
export type MatchPlayerCreateInput = { export type MatchPlayerCreateInput = {
@ -24757,6 +24813,8 @@ export namespace Prisma {
matchDate?: SortOrder matchDate?: SortOrder
createdAt?: SortOrder createdAt?: SortOrder
updatedAt?: SortOrder updatedAt?: SortOrder
cs2MatchId?: SortOrder
exportedAt?: SortOrder
} }
export type MatchAvgOrderByAggregateInput = { export type MatchAvgOrderByAggregateInput = {
@ -24764,6 +24822,7 @@ export namespace Prisma {
scoreB?: SortOrder scoreB?: SortOrder
roundCount?: SortOrder roundCount?: SortOrder
bestOf?: SortOrder bestOf?: SortOrder
cs2MatchId?: SortOrder
} }
export type MatchMaxOrderByAggregateInput = { export type MatchMaxOrderByAggregateInput = {
@ -24784,6 +24843,8 @@ export namespace Prisma {
matchDate?: SortOrder matchDate?: SortOrder
createdAt?: SortOrder createdAt?: SortOrder
updatedAt?: SortOrder updatedAt?: SortOrder
cs2MatchId?: SortOrder
exportedAt?: SortOrder
} }
export type MatchMinOrderByAggregateInput = { export type MatchMinOrderByAggregateInput = {
@ -24804,6 +24865,8 @@ export namespace Prisma {
matchDate?: SortOrder matchDate?: SortOrder
createdAt?: SortOrder createdAt?: SortOrder
updatedAt?: SortOrder updatedAt?: SortOrder
cs2MatchId?: SortOrder
exportedAt?: SortOrder
} }
export type MatchSumOrderByAggregateInput = { export type MatchSumOrderByAggregateInput = {
@ -24811,6 +24874,7 @@ export namespace Prisma {
scoreB?: SortOrder scoreB?: SortOrder
roundCount?: SortOrder roundCount?: SortOrder
bestOf?: SortOrder bestOf?: SortOrder
cs2MatchId?: SortOrder
} }
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> = export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
| PatchUndefined< | PatchUndefined<
@ -27636,6 +27700,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamBUsers?: UserCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserCreateNestedManyWithoutMatchesAsTeamBInput
@ -27667,6 +27733,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
players?: MatchPlayerUncheckedCreateNestedManyWithoutMatchInput players?: MatchPlayerUncheckedCreateNestedManyWithoutMatchInput
@ -27699,6 +27767,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
@ -27730,6 +27800,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
players?: MatchPlayerUncheckedCreateNestedManyWithoutMatchInput players?: MatchPlayerUncheckedCreateNestedManyWithoutMatchInput
@ -28173,6 +28245,8 @@ export namespace Prisma {
matchDate?: DateTimeNullableFilter<"Match"> | Date | string | null matchDate?: DateTimeNullableFilter<"Match"> | Date | string | null
createdAt?: DateTimeFilter<"Match"> | Date | string createdAt?: DateTimeFilter<"Match"> | Date | string
updatedAt?: DateTimeFilter<"Match"> | Date | string updatedAt?: DateTimeFilter<"Match"> | Date | string
cs2MatchId?: IntNullableFilter<"Match"> | number | null
exportedAt?: DateTimeNullableFilter<"Match"> | Date | string | null
} }
export type MatchUpsertWithWhereUniqueWithoutTeamBUsersInput = { export type MatchUpsertWithWhereUniqueWithoutTeamBUsersInput = {
@ -28670,6 +28744,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserCreateNestedManyWithoutMatchesAsTeamBInput
@ -28700,6 +28776,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
@ -28738,6 +28816,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserCreateNestedManyWithoutMatchesAsTeamBInput
@ -28768,6 +28848,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
@ -30174,6 +30256,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
@ -30205,6 +30289,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
@ -30425,6 +30511,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
@ -30456,6 +30544,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
@ -30738,6 +30828,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
@ -30769,6 +30861,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
@ -30881,6 +30975,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
@ -30912,6 +31008,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
@ -31143,6 +31241,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
@ -31174,6 +31274,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
@ -31445,6 +31547,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
@ -31476,6 +31580,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
@ -31503,6 +31609,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
@ -31534,6 +31642,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
players?: MatchPlayerUncheckedCreateNestedManyWithoutMatchInput players?: MatchPlayerUncheckedCreateNestedManyWithoutMatchInput
@ -31640,6 +31750,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
@ -31671,6 +31783,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
players?: MatchPlayerUncheckedUpdateManyWithoutMatchNestedInput players?: MatchPlayerUncheckedUpdateManyWithoutMatchNestedInput
@ -31899,6 +32013,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
@ -31930,6 +32046,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
@ -32003,6 +32121,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
@ -32034,6 +32154,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
@ -32369,6 +32491,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput teamA?: TeamCreateNestedOneWithoutMatchesAsTeamAInput
teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput teamB?: TeamCreateNestedOneWithoutMatchesAsTeamBInput
teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserCreateNestedManyWithoutMatchesAsTeamAInput
@ -32400,6 +32524,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput teamAUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamAInput
teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput teamBUsers?: UserUncheckedCreateNestedManyWithoutMatchesAsTeamBInput
demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput demoFile?: DemoFileUncheckedCreateNestedOneWithoutMatchInput
@ -32506,6 +32632,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
@ -32537,6 +32665,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
@ -32732,6 +32862,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamBUsers?: UserUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUpdateManyWithoutMatchesAsTeamBNestedInput
@ -32763,6 +32895,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
players?: MatchPlayerUncheckedUpdateManyWithoutMatchNestedInput players?: MatchPlayerUncheckedUpdateManyWithoutMatchNestedInput
@ -32792,6 +32926,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
} }
export type MatchUpdateWithoutTeamBUsersInput = { export type MatchUpdateWithoutTeamBUsersInput = {
@ -32812,6 +32948,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
@ -32843,6 +32981,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
players?: MatchPlayerUncheckedUpdateManyWithoutMatchNestedInput players?: MatchPlayerUncheckedUpdateManyWithoutMatchNestedInput
@ -32872,6 +33012,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
} }
export type TeamInviteUpdateWithoutUserInput = { export type TeamInviteUpdateWithoutUserInput = {
@ -33222,6 +33364,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
} }
export type MatchCreateManyTeamBInput = { export type MatchCreateManyTeamBInput = {
@ -33243,6 +33387,8 @@ export namespace Prisma {
matchDate?: Date | string | null matchDate?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
cs2MatchId?: number | null
exportedAt?: Date | string | null
} }
export type ScheduleCreateManyTeamAInput = { export type ScheduleCreateManyTeamAInput = {
@ -33421,6 +33567,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput teamB?: TeamUpdateOneWithoutMatchesAsTeamBNestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUpdateManyWithoutMatchesAsTeamBNestedInput
@ -33451,6 +33599,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
@ -33480,6 +33630,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
} }
export type MatchUpdateWithoutTeamBInput = { export type MatchUpdateWithoutTeamBInput = {
@ -33500,6 +33652,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput teamA?: TeamUpdateOneWithoutMatchesAsTeamANestedInput
teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUpdateManyWithoutMatchesAsTeamBNestedInput
@ -33530,6 +33684,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput teamAUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamANestedInput
teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput teamBUsers?: UserUncheckedUpdateManyWithoutMatchesAsTeamBNestedInput
demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput demoFile?: DemoFileUncheckedUpdateOneWithoutMatchNestedInput
@ -33559,6 +33715,8 @@ export namespace Prisma {
matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null matchDate?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
cs2MatchId?: NullableIntFieldUpdateOperationsInput | number | null
exportedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
} }
export type ScheduleUpdateWithoutTeamAInput = { export type ScheduleUpdateWithoutTeamAInput = {

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"name": "prisma-client-85c440bbfe4ddbdbf4749495c6ef753c2d4a73eb44baae0e95774ed8e7b86d85", "name": "prisma-client-dccc49918d4c87081feec825d7e8f9d86eb03460cb722e1aeb3c8b2bf7d2181c",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"browser": "default.js", "browser": "default.js",

File diff suppressed because one or more lines are too long