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?
readyAcceptances MatchReady[] @relation("MatchReadyMatch")
cs2MatchId Int? // die in die JSON geschriebene matchid
exportedAt DateTime? // wann die JSON exportiert wurde
}
model MatchPlayer {

View File

@ -1,4 +1,4 @@
// src/app/admin/page.tsx
// /src/app/admin/page.tsx
import { redirect } from 'next/navigation'
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'
import { useCallback, useEffect, useState, useRef } from 'react'

View File

@ -1,3 +1,5 @@
// /src/app/admin/teams/page.tsx
'use client'
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'
/** 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 { getServerSession } from 'next-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 { getServerSession } from 'next-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 { 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));
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() {
try {
// 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 ---------- */
@ -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)
if (chosen.length < bestOf) return
// ⬇️ JSON bauen (enthält cs2MatchId/rndId)
const json = buildMatchJson(mLike, sLike)
const jsonStr = JSON.stringify(json, null, 2)
const filename = `${match.id}.json`
// --- SFTP Upload wie gehabt ---
const url = process.env.PTERO_SERVER_SFTP_URL || ''
const user = process.env.PTERO_SERVER_SFTP_USER
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}`)
// 👇 NACH ERFOLGREICHEM UPLOAD: Match in CS2-Plugin laden
// Laut Vorgabe nur die JSON-Datei als Argument übergeben:
// erst aktuelles Match beenden/entladen …
await unloadCurrentMatch()
// … dann das neue laden
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) {
console.error('[mapvote] Export fehlgeschlagen:', err)
}
}
/* ---------- kleine Helfer für match-ready Payload ---------- */
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 { prisma } from '@/app/lib/prisma'
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 { getServerSession } from 'next-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 { 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) {
const session = await getServerSession(authOptions(req))
@ -11,9 +12,19 @@ export async function GET(req: NextRequest) {
}
const notifications = await prisma.notification.findMany({
where: { steamId: session.user.steamId },
where: {
steamId: session.user.steamId,
},
orderBy: { createdAt: 'desc' },
take: 10,
select: {
id: true,
title: true,
message: true,
read: true,
actionType: true,
actionData: true,
createdAt: true,
},
})
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'
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 { prisma } from '@/app/lib/prisma'
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 { getServerSession } from 'next-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 { prisma } from '@/app/lib/prisma'
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 { prisma } from '@/app/lib/prisma'
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 { prisma } from '@/app/lib/prisma'
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 { prisma } from '@/app/lib/prisma'
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 { NextResponse, type NextRequest } from 'next/server'
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 { prisma } from '@/app/lib/prisma'
import type { Player } from '@/app/types/team'
@ -41,14 +41,16 @@ export async function GET() {
id: t.id,
name: t.name,
logo: t.logo,
leader: t.leaderId,
leaderId: t.leaderId,
createdAt: t.createdAt,
activePlayers: t.activePlayers .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(result, { headers: { 'Cache-Control': 'no-store' } })
return NextResponse.json(
{ items: result, hasMore: false },
{ headers: { 'Cache-Control': 'no-store' } }
)
} catch (err) {
console.error('GET /api/teams failed:', err)
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 { 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 { getServerSession } from 'next-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 { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth'

View File

@ -10,7 +10,10 @@ type ButtonProps = {
modalId?: string
color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent'
variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link'
/** Steuert NUR Höhe/Abstände */
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
/** Optionale Schriftgröße */
textSize?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl'
className?: string
dropDirection?: 'up' | 'down' | 'auto'
disabled?: boolean
@ -27,6 +30,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
color = 'blue',
variant = 'solid',
size = 'md',
textSize = 'sm',
className,
dropDirection = 'down',
disabled = false,
@ -49,18 +53,31 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
}
: {}
const sizeClasses: Record<string, string> = {
xs: 'py-1 px-2',
sm: 'py-2 px-3',
md: 'py-3 px-4',
lg: 'p-4 sm:p-5',
xl: 'py-6 px-8 text-lg',
full: 'py-6 px-8 text-lg w-full',
// Feste Höhen sorgen für vertikale Zentrierung
const sizeClasses: Record<NonNullable<ButtonProps['size']>, string> = {
xs: 'h-7 px-2',
sm: 'h-8 px-3',
md: 'h-9 px-4',
lg: 'h-10 px-5',
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 = `
${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
`
@ -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',
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',
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: {
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',
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',
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',
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',
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-white hover:bg-white/10 focus:bg-white/10 dark:text-white',
},
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',
@ -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',
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',
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: {
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 spaceBelow = window.innerHeight - rect.bottom
const spaceAbove = rect.top
if (spaceBelow < dropdownHeight && spaceAbove > dropdownHeight) {
setDirection('up')
} else {
setDirection('down')
}
setDirection(spaceBelow < dropdownHeight && spaceAbove > dropdownHeight ? 'up' : 'down')
})
}
}, [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"
role="status"
aria-label="loading"
></span>
/>
)}
{children ?? title}
</button>

View File

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

View File

@ -1,4 +1,4 @@
// /app/components/MapVoteBanner.tsx
// /src/app/components/MapVoteBanner.tsx
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
@ -24,7 +24,6 @@ function formatCountdown(ms: number) {
const pad = (n:number)=>String(n).padStart(2,'0')
return `${h}:${pad(m)}:${pad(s)}`
}
function formatLead(minutes: number) {
if (!Number.isFinite(minutes) || minutes <= 0) return 'zum Matchbeginn'
const h = Math.floor(minutes / 60)
@ -35,11 +34,7 @@ function formatLead(minutes: number) {
}
export default function MapVoteBanner({
match,
initialNow,
matchBaseTs,
sseOpensAtTs,
sseLeadMinutes,
match, initialNow, matchBaseTs, sseOpensAtTs, sseLeadMinutes,
}: Props) {
const router = useRouter()
const { data: session } = useSession()
@ -50,16 +45,11 @@ export default function MapVoteBanner({
const [leadOverride, setLeadOverride] = 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)
useEffect(() => { setMounted(true) }, [])
// clientseitiger Ticker
const [now, setNow] = useState(initialNow)
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000)
return () => clearInterval(id)
}, [])
useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id) }, [])
const load = useCallback(async () => {
try {
@ -78,16 +68,11 @@ export default function MapVoteBanner({
}
}, [match.id])
// initial + bei Meta-Änderungen
useEffect(() => { load() }, [load])
useEffect(() => { load() }, [match.matchDate, match.demoDate, match.bestOf, load])
const matchDateTs = useMemo(
() => (typeof matchBaseTs === 'number' ? matchBaseTs : null),
[matchBaseTs]
)
const matchDateTs = useMemo(() => (typeof matchBaseTs === 'number' ? matchBaseTs : null), [matchBaseTs])
// SSE: nur map-vote-updated & Co. beachten
useEffect(() => {
if (!lastEvent) return
const { type } = lastEvent as any
@ -107,7 +92,6 @@ export default function MapVoteBanner({
? (typeof evt.opensAt === 'string' ? evt.opensAt : new Date(evt.opensAt).toISOString())
: undefined
// Sofort lokale Overrides setzen
if (nextOpensAtISO) {
setOpensAtOverride(new Date(nextOpensAtISO).getTime())
} else if (Number.isFinite(parsedLead) && matchDateTs != null) {
@ -115,7 +99,6 @@ export default function MapVoteBanner({
}
if (Number.isFinite(parsedLead)) setLeadOverride(parsedLead as number)
// sichtbares Mergen (für UI-Texte)
if (nextOpensAtISO !== undefined || Number.isFinite(parsedLead)) {
setState(prev => ({
...(prev ?? {} as any),
@ -127,7 +110,6 @@ export default function MapVoteBanner({
}
}, [lastEvent, match.id, matchDateTs, load])
// Öffnet wann? (Priorität: Parent-SSE → lokale SSE → Server → Fallback)
const opensAt = useMemo(() => {
if (typeof sseOpensAtTs === 'number') return sseOpensAtTs
if (opensAtOverride != null) return opensAtOverride
@ -139,11 +121,8 @@ export default function MapVoteBanner({
return matchDateTs - lead * 60_000
}, [sseOpensAtTs, opensAtOverride, state?.opensAt, matchDateTs, initialNow, sseLeadMinutes, leadOverride, state?.leadMinutes])
// „startet X vor Matchbeginn“
const leadMinutes = useMemo(() => {
if (matchDateTs != null && opensAt != null) {
return Math.max(0, Math.round((matchDateTs - opensAt) / 60_000))
}
if (matchDateTs != null && opensAt != null) return Math.max(0, Math.round((matchDateTs - opensAt) / 60_000))
if (typeof sseLeadMinutes === 'number') return sseLeadMinutes
if (leadOverride != null) return leadOverride
if (Number.isFinite(state?.leadMinutes)) return state!.leadMinutes as number
@ -152,6 +131,8 @@ export default function MapVoteBanner({
const isOpen = mounted && now >= opensAt
const msToOpen = Math.max(opensAt - now, 0)
const isLocked = !!state?.locked
const isVotingOpen = isOpen && !isLocked
const current = state?.steps?.[state?.currentIndex ?? 0]
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 isAdmin = !!session?.user?.isAdmin
const iCanAct = Boolean(
isOpen &&
!state?.locked &&
isVotingOpen &&
current?.teamId &&
(isAdmin ||
(current.teamId === match.teamA?.id && isLeaderA) ||
@ -172,12 +152,24 @@ export default function MapVoteBanner({
const gotoFullPage = () => router.push(`/match-details/${match.id}/vote`)
const cardClasses =
'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 ' +
(isOpen
? 'ring-1 ring-green-500/15 hover:ring-green-500/30 hover:shadow-lg'
: 'ring-1 ring-neutral-500/10 hover:ring-neutral-500/20 hover:shadow-md')
// Farblogik: locked → grün, offen → gelb, noch geschlossen → neutral
const ringClass = isLocked
? 'ring-1 ring-green-500/15 hover:ring-green-500/30 hover:shadow-lg'
: isVotingOpen
? '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'
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 (
<div
@ -185,41 +177,35 @@ export default function MapVoteBanner({
tabIndex={0}
onClick={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"
>
{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" />
</>
)}
<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="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">
<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>
</div>
<div className="min-w-0">
<div className="font-medium text-gray-900 dark:text-neutral-100">
Map-Vote
</div>
<div className="font-medium text-gray-900 dark:text-neutral-100">Map-Vote</div>
<div className="text-xs text-gray-600 dark:text-neutral-400 truncate">
Modus: BO{match.bestOf ?? state?.bestOf ?? 3}
{state?.locked
? ' • Auswahl fixiert'
: isOpen
: isVotingOpen
? (whoIsUp ? ` • am Zug: ${whoIsUp}` : ' • läuft')
: ` • startet ${formatLead(leadMinutes)} vor Matchbeginn`}
</div>
{error && (
<div className="text-xs text-red-600 dark:text-red-400 mt-0.5">
{error}
</div>
)}
{error && <div className="text-xs text-red-600 dark:text-red-400 mt-0.5">{error}</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">
Voting abgeschlossen
</span>
) : isOpen ? (
<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">
) : isVotingOpen ? (
<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'}
</span>
) : (
// 🔑 Hydration-safe: vor dem Mount nur ein Placeholder rendern
<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"
suppressHydrationWarning
@ -245,63 +230,43 @@ export default function MapVoteBanner({
</div>
<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% } to { background-position-x: 200% } }
.mapVoteGradient--green {
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 {
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;
}
: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%
);
:global(.dark) .mapVoteGradient--green {
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 {
0% { transform: translateX(-120%) skewX(-20deg); opacity: 0; }
10% { opacity: .7; }
27% { transform: translateX(120%) skewX(-20deg); opacity: 0; }
100% { transform: translateX(120%) skewX(-20deg); opacity: 0; }
}
.shine {
position: absolute;
inset: 0;
0% { transform: translateX(-120%) skewX(-20deg); opacity: 0 }
10% { opacity: .7 }
27% { transform: translateX(120%) skewX(-20deg); opacity: 0 }
100% { transform: translateX(120%) skewX(-20deg); opacity: 0 }
}
.shine { position: absolute; inset: 0 }
.shine::before {
content: "";
position: absolute;
top: -25%;
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;
content: ""; position: absolute; top: -25%; 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 }
@media (prefers-reduced-motion: reduce) {
.mapVoteGradient { animation: none; }
.shine::before { animation: none !important; transform: none !important; opacity: 0 !important; }
.mapVoteGradient--green, .mapVoteGradient--yellow { animation: none }
.shine::before { animation: none !important; transform: none !important; opacity: 0 !important }
}
`}</style>
</div>

View File

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

View File

@ -1,4 +1,4 @@
// /app/components/MatchDetails.tsx
// /src/app/components/MatchDetails.tsx
'use client'
import { useState, useEffect, useMemo, useRef } from 'react'
@ -431,13 +431,15 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
</div>
{/* MapVote-Banner erhält die aktuell berechneten (SSE-konformen) Werte */}
<MapVoteBanner
match={match}
initialNow={initialNow}
matchBaseTs={matchBaseTs}
sseOpensAtTs={sseOpensAtTs}
sseLeadMinutes={sseLeadMinutes}
/>
{(match.matchType === 'community' &&
<MapVoteBanner
match={match}
initialNow={initialNow}
matchBaseTs={matchBaseTs}
sseOpensAtTs={sseOpensAtTs}
sseLeadMinutes={sseLeadMinutes}
/>
)}
{/* ───────── Team-Blöcke ───────── */}
<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 { useSSEStore } from '@/app/lib/useSSEStore'
import { NOTIFICATION_EVENTS, isSseEventType } from '../lib/sseEvents'
import { useUiChromeStore } from '@/app/lib/useUiChromeStore'
type Notification = {
id: string
@ -36,6 +37,7 @@ export default function NotificationBell() {
const router = useRouter()
const { lastEvent } = useSSEStore() // nur konsumieren, nicht connecten
const bellRef = useRef<HTMLButtonElement | null>(null);
const telemetryBannerPx = useUiChromeStore(s => s.telemetryBannerPx)
const [notifications, setNotifications] = useState<Notification[]>([])
const [open, setOpen] = useState(false)
@ -43,6 +45,9 @@ export default function NotificationBell() {
const [showPreview, setShowPreview] = useState(false)
const [animateBell, setAnimateBell] = useState(false)
const baseBottom = 24 // px, entspricht bottom-6
const bottomPx = baseBottom + (telemetryBannerPx || 0)
useEffect(() => {
if (!lastEvent) return
if (!isSseEventType(lastEvent.type)) return
@ -79,7 +84,7 @@ export default function NotificationBell() {
if (!steamId) return
;(async () => {
try {
const res = await fetch('/api/notifications/user')
const res = await fetch('/api/notifications')
if (!res.ok) throw new Error('Fehler beim Laden')
const data = await res.json()
const loaded: Notification[] = data.notifications.map((n: any) => ({
@ -257,7 +262,10 @@ export default function NotificationBell() {
// 4) Render
return (
<div className="fixed bottom-6 right-6 z-50">
<div
className="fixed right-6 z-50"
style={{ bottom: bottomPx }}
>
<button
ref={bellRef}
type="button"

View File

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

View File

@ -182,35 +182,6 @@ export default function Sidebar() {
Spielplan
</Button>
</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>
</nav>

View File

@ -68,11 +68,11 @@ export default function SidebarFooter() {
}`
return (
<div className="relative w-full">
<div className="relative w-full min-h-[65px]">
{/* Kopf / Toggle */}
<button
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'}
`}
>

View File

@ -1,6 +1,7 @@
// /src/app/components/TeamCard.tsx
'use client'
import { useState } from 'react'
import { useState, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import Button from './Button'
import TeamPremierRankBadge from './TeamPremierRankBadge'
@ -12,6 +13,9 @@ type Props = {
invitationId?: string
onUpdateInvitation: (teamId: string, newValue: string | null | 'pending') => void
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({
@ -20,15 +24,29 @@ export default function TeamCard({
invitationId,
onUpdateInvitation,
adminMode = false,
canRequestJoin = true,
}: Props) {
const router = useRouter()
const [joining, setJoining] = useState(false)
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 () => {
if (joining) return
if (joining || isDisabled) return
setJoining(true)
try {
if (isRequested) {
@ -55,6 +73,27 @@ export default function TeamCard({
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 (
<div
role="button"
@ -96,27 +135,16 @@ export default function TeamCard({
Verwalten
</Button>
) : (
// 👉 Button immer zeigen falls nicht möglich: disabled + anderes Label
<Button
title={isRequested ? 'Angefragt (zurückziehen)' : 'Beitritt anfragen'}
title={typeof buttonLabel === 'string' ? buttonLabel : undefined}
size="sm"
color={isRequested ? 'gray' : 'blue'}
color={buttonColor as any}
disabled={isDisabled}
onClick={e => { e.stopPropagation(); handleClick() }}
aria-disabled={isDisabled ? 'true' : undefined}
>
{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
</>
) : isRequested ? (
'Angefragt'
) : (
'Beitritt anfragen'
)}
{buttonLabel}
</Button>
)}
</div>

View File

@ -1,4 +1,4 @@
// /app/components/TeamCardComponent.tsx
// /src/app/components/TeamCardComponent.tsx
'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'
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 { useTelemetryStore } from '@/app/lib/useTelemetryStore'
import { useMatchRosterStore } from '@/app/lib/useMatchRosterStore'
import { useSession } from 'next-auth/react'
type Status = 'idle'|'connecting'|'open'|'closed'|'error'
import TelemetryBanner from './TelemetryBanner'
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
function makeWsUrl(host?: string, port?: string, path?: string, scheme?: string) {
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 useWss = sch === 'wss' || (sch !== 'ws' && (p === '443' || pageHttps))
const proto = useWss ? 'wss' : 'ws'
const portPart = (p === '80' || p === '443') ? '' : `:${p}`
const portPart = p === '80' || p === '443' ? '' : `:${p}`
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() {
const url = useMemo(
() => makeWsUrl(
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_HOST,
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PORT,
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PATH,
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_SCHEME
),
() =>
makeWsUrl(
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_HOST,
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PORT,
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PATH,
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_SCHEME
),
[]
)
const { data: session } = useSession()
const mySteamId = (session?.user as any)?.steamId ?? null
const setSnapshot = usePresenceStore(s => s.setSnapshot)
const setJoin = usePresenceStore(s => s.setJoin)
const setLeave = usePresenceStore(s => s.setLeave)
// overlay control
const hideOverlay = useReadyOverlayStore((s) => s.hide)
const setMapKey = useTelemetryStore(s => s.setMapKey)
const phase = useTelemetryStore(s => s.phase)
const setPhase = useTelemetryStore(s => s.setPhase)
// presence/telemetry stores
const setSnapshot = usePresenceStore((s) => s.setSnapshot)
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
const [myConnected, setMyConnected] = useState(false)
// roster (persisted by ReadyOverlayHost)
const rosterSet = useMatchRosterStore((s) => s.roster)
// internes Dismiss-Flag fürs Banner (pro Mount)
const [dismissed, setDismissed] = useState(false)
// local telemetry state
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 [serverName, setServerName] = useState<string | null>(null)
// WS-Reconnect
// ws
const aliveRef = useRef(true)
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(() => {
(async () => {
if (typeof window === 'undefined') return
setDockEl(document.getElementById('telemetry-banner-dock') as HTMLElement | null)
}, [])
// connect href from API
useEffect(() => {
;(async () => {
try {
const r = await fetch('/api/cs2/server', { cache: 'no-store' })
if (r.ok) {
@ -71,6 +112,7 @@ export default function TelemetrySocket() {
})()
}, [])
// websocket connect
useEffect(() => {
aliveRef.current = true
@ -95,40 +137,70 @@ export default function TelemetrySocket() {
try { msg = JSON.parse(String(ev.data ?? '')) } catch {}
if (!msg) return
if (msg.type === 'players' && Array.isArray(msg.players)) {
setSnapshot(msg.players)
// 👇 bin ich in der aktuellen Players-Liste?
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)
// --- server name (optional)
if (msg.type === 'server' && typeof msg.name === 'string' && msg.name.trim()) {
setServerName(msg.name.trim())
}
// Map (inkl. optionaler Phase)
if (msg.type === 'map' && typeof msg.name === 'string') {
const key = msg.name.toLowerCase()
if (process.env.NODE_ENV!=='production') console.debug('[TelemetrySocket] map:', key)
setMapKey(key)
if (typeof msg.phase === 'string') {
setPhase(String(msg.phase).toLowerCase() as any)
// --- full roster
if (msg.type === 'players' && Array.isArray(msg.players)) {
setSnapshot(msg.players)
const ids = msg.players.map(sidOf).filter(Boolean)
setTelemetrySet(toSet(ids))
if (mySteamId) {
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') {
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)
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:
// - Ich bin im Roster (Match wurde geladen & ich bin Teilnehmer)
// - Match-Phase live
// - Ich bin NICHT connected (disconnect)
const meInRoster = !!mySteamId && rosterSet instanceof Set && rosterSet.has(String(mySteamId))
const shouldShow = !dismissed && (phase === 'live') && meInRoster && !myConnected
// ----- banner logic (connected + disconnected variants) with roster fallback
const myId = mySteamId ? String(mySteamId) : null
const roster =
rosterSet instanceof Set && rosterSet.size > 0
? rosterSet
: (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 =
connectHref // ⬅️ bevorzugt API-Route (inkl. Passwort)
|| process.env.NEXT_PUBLIC_STEAM_CONNECT_URI
|| process.env.NEXT_PUBLIC_CS2_CONNECT_URI
|| 'steam://rungameid/730//+retry' // Fallback
connectHref ||
process.env.NEXT_PUBLIC_STEAM_CONNECT_URI ||
process.env.NEXT_PUBLIC_CS2_CONNECT_URI ||
'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 = () => {
try {
window.location.href = connectUri
} catch {
// no-op
}
try { window.location.href = connectUri } catch {}
}
return (
<>
{/* WebSocket-Client selbst rendert nichts */}
{shouldShow && (
<div className="fixed inset-x-0 bottom-0 z-[9999] mx-auto mb-3 max-w-3xl">
<div className="mx-3 rounded-md bg-neutral-900/95 text-white shadow-lg ring-1 ring-black/10">
<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>
<div className="text-xs opacity-80">
Du bist im aufgesetzten Match eingetragen. Klicke Reconnect, um wieder zu joinen.
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleReconnect}
className="px-3 py-1.5 rounded bg-emerald-500 hover:bg-emerald-600 text-sm font-semibold"
>
Reconnect
</button>
<button
onClick={() => setDismissed(true)}
className="px-2 py-1 rounded bg-neutral-700 hover:bg-neutral-600 text-xs"
aria-label="schließen"
>
</button>
</div>
</div>
</div>
</div>
)}
</>
const handleDisconnect = () => {
// Auto-Reconnect stoppen
aliveRef.current = false;
if (retryRef.current) {
window.clearTimeout(retryRef.current);
retryRef.current = null;
}
// WebSocket sauber schließen
try { wsRef.current?.close(1000, 'user requested disconnect') } catch {}
wsRef.current = null;
// Lokalen Zustand zurücksetzen (wir bleiben im "disconnected"-Banner)
setTelemetrySet(new Set());
setServerName(null);
setMapKeyForUi(null);
setPhase('unknown' as any);
setScore({ a: null, b: null });
};
const variant: 'connected' | 'disconnected' = iAmOnline ? 'connected' : 'disconnected'
const visible = iAmExpected
const zIndex = iAmOnline ? 9998 : 9999
const bannerEl = (
<TelemetryBanner
variant={variant}
visible={visible}
zIndex={zIndex}
inline={!!dockEl}
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'
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 PremierRankBadge from './PremierRankBadge'

View File

@ -1,3 +1,5 @@
// /src/app/components/admin/teams/AdminTeamsView.tsx
'use client'
import { useEffect, useState, useRef, useCallback } from 'react'
@ -25,9 +27,15 @@ export default function AdminTeamsView() {
const fetchTeams = useCallback(async () => {
setLoading(true)
try {
const res = await fetch('/api/teams')
const res = await fetch('/api/teams', { cache: 'no-store' })
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) {
console.error('[AdminTeamsView] /api/teams:', err)
setTeams([])

View File

@ -1,3 +1,5 @@
// /src/app/components/profile/[steamId]/matches/UserMatchesList.tsx
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
@ -230,13 +232,47 @@ export default function UserMatchesList({ steamId }: { steamId: string }) {
>
<Table.Cell hoverable>
<div className="flex items-center gap-2">
<img
src={`/assets/img/mapicons/${m.map}.webp`}
alt={mapInfo?.label}
width={32}
height={32}
/>
{mapInfo?.label}
{(() => {
const raw = m.map || ''
const normKey = raw.replace(/^de_/, '') // "de_ancient" -> "ancient"
// 1) Versuch: exact normKey
// 2) Versuch: raw (mit "de_")
// 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>
</Table.Cell>

View File

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

View File

@ -1,76 +1,54 @@
// /src/app/dashboard/page.tsx
'use client'
import { useEffect, useState } from '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() {
const { data: session, status } = useSession()
const [showTeamModal, setShowTeamModal] = useState(false)
const [teams, setTeams] = useState<string[]>([])
const [selectedTeam, setSelectedTeam] = useState('')
// Teams laden (robust)
useEffect(() => {
if (status === 'authenticated' && !session?.user?.team) {
setShowTeamModal(true)
}
}, [session, status])
let abort = false
useEffect(() => {
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 () => {
async function fetchTeams() {
try {
const res = await fetch('/api/teams')
const data = await res.json()
setTeams(data.teams.map((t: { teamname: string }) => t.teamname))
const res = await fetch('/api/teams', { cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
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) {
console.error('Fehler beim Laden der Teams:', error)
if (!abort) setTeams([])
}
}
fetchTeams()
return () => { abort = true }
}, [])
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">
Willkommen im Dashboard!
</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}
</div>
</main>
<div id="telemetry-banner-dock" className="h-full max-h-[65px]" />
</div>
</div>

View File

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

View File

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

View File

@ -1,4 +1,4 @@
// src/app/lib/signOutWithStatus.ts
// /src/app/lib/signOutWithStatus.ts
'use client'
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 = [
// Kanonisch

View File

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

View File

@ -1,4 +1,4 @@
// /app/lib/useTelemetryStore.ts
// /src/app/lib/useTelemetryStore.ts
import { create } from 'zustand'
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 { notFound } from 'next/navigation'
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 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'
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 { 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'
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'
import { useEffect, useState, KeyboardEvent, MouseEvent } from 'react'

View File

@ -1,4 +1,4 @@
// /app/team/page.tsx
// /src/app/team/page.tsx
'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'

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long