updated mapvote
This commit is contained in:
parent
6caf57d282
commit
a832abff2e
@ -44,7 +44,7 @@ model User {
|
|||||||
createdSchedules Schedule[] @relation("CreatedSchedules")
|
createdSchedules Schedule[] @relation("CreatedSchedules")
|
||||||
confirmedSchedules Schedule[] @relation("ConfirmedSchedules")
|
confirmedSchedules Schedule[] @relation("ConfirmedSchedules")
|
||||||
|
|
||||||
mapVetoChoices MapVetoStep[] @relation("VetoStepChooser")
|
mapVoteChoices MapVoteStep[] @relation("VoteStepChooser")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Team {
|
model Team {
|
||||||
@ -68,7 +68,7 @@ model Team {
|
|||||||
schedulesAsTeamA Schedule[] @relation("ScheduleTeamA")
|
schedulesAsTeamA Schedule[] @relation("ScheduleTeamA")
|
||||||
schedulesAsTeamB Schedule[] @relation("ScheduleTeamB")
|
schedulesAsTeamB Schedule[] @relation("ScheduleTeamB")
|
||||||
|
|
||||||
mapVetoSteps MapVetoStep[] @relation("VetoStepTeam")
|
mapVoteSteps MapVoteStep[] @relation("VoteStepTeam")
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamInvite {
|
model TeamInvite {
|
||||||
@ -138,7 +138,7 @@ model Match {
|
|||||||
|
|
||||||
bestOf Int @default(3) // 1 | 3 | 5 – app-seitig validieren
|
bestOf Int @default(3) // 1 | 3 | 5 – app-seitig validieren
|
||||||
matchDate DateTime? // geplante Startzeit (separat von demoDate)
|
matchDate DateTime? // geplante Startzeit (separat von demoDate)
|
||||||
mapVeto MapVeto? // 1:1 Map-Vote-Status
|
mapVote MapVote?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@ -297,51 +297,50 @@ model ServerRequest {
|
|||||||
// 🗺️ Map-Vote
|
// 🗺️ Map-Vote
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
enum MapVetoAction {
|
enum MapVoteAction {
|
||||||
BAN
|
BAN
|
||||||
PICK
|
PICK
|
||||||
DECIDER
|
DECIDER
|
||||||
}
|
}
|
||||||
|
|
||||||
model MapVeto {
|
model MapVote {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
matchId String @unique
|
matchId String @unique
|
||||||
match Match @relation(fields: [matchId], references: [id])
|
match Match @relation(fields: [matchId], references: [id])
|
||||||
|
|
||||||
// Basiszustand
|
|
||||||
bestOf Int @default(3)
|
bestOf Int @default(3)
|
||||||
mapPool String[] // z.B. ["de_inferno","de_mirage",...]
|
mapPool String[]
|
||||||
currentIdx Int @default(0)
|
currentIdx Int @default(0)
|
||||||
locked Boolean @default(false)
|
locked Boolean @default(false)
|
||||||
|
opensAt DateTime?
|
||||||
|
|
||||||
// Optional: serverseitig speichern, statt im UI zu berechnen
|
adminEditingBy String?
|
||||||
opensAt DateTime?
|
adminEditingSince DateTime?
|
||||||
|
|
||||||
steps MapVetoStep[]
|
steps MapVoteStep[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model MapVetoStep {
|
model MapVoteStep {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
vetoId String
|
voteId String
|
||||||
order Int
|
order Int
|
||||||
action MapVetoAction
|
action MapVoteAction
|
||||||
|
|
||||||
// Team, das am Zug ist (kann bei DECIDER null sein)
|
|
||||||
teamId String?
|
teamId String?
|
||||||
team Team? @relation("VetoStepTeam", fields: [teamId], references: [id])
|
team Team? @relation("VoteStepTeam", fields: [teamId], references: [id])
|
||||||
|
|
||||||
// Ergebnis & wer gewählt hat
|
|
||||||
map String?
|
map String?
|
||||||
chosenAt DateTime?
|
chosenAt DateTime?
|
||||||
chosenBy String?
|
chosenBy String?
|
||||||
chooser User? @relation("VetoStepChooser", fields: [chosenBy], references: [steamId])
|
chooser User? @relation("VoteStepChooser", fields: [chosenBy], references: [steamId])
|
||||||
|
|
||||||
veto MapVeto @relation(fields: [vetoId], references: [id])
|
vote MapVote @relation(fields: [voteId], references: [id])
|
||||||
|
|
||||||
@@unique([vetoId, order])
|
@@unique([voteId, order])
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
@@index([chosenBy])
|
@@index([chosenBy])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
public/assets/img/maps/de_nuke/1.jpg
Normal file
BIN
public/assets/img/maps/de_nuke/1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@ -56,7 +56,7 @@ export async function buildCommunityFuturePayload(m: any) {
|
|||||||
.sort((a: any, b: any) => (a.user.name || '').localeCompare(b.user.name || ''))
|
.sort((a: any, b: any) => (a.user.name || '').localeCompare(b.user.name || ''))
|
||||||
|
|
||||||
const startTs = computeStartTs(m)
|
const startTs = computeStartTs(m)
|
||||||
const editableUntil = startTs - 60 * 60 * 1000 // 1h vor Start/Veto
|
const editableUntil = startTs - 60 * 60 * 1000 // 1h vor Start/Vote
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id : m.id,
|
id : m.id,
|
||||||
|
|||||||
@ -26,8 +26,8 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
await tx.mapVetoStep.deleteMany({ where: { veto: { matchId } } })
|
await tx.mapVoteStep.deleteMany({ where: { vote: { matchId } } })
|
||||||
await tx.mapVeto.deleteMany({ where: { matchId } })
|
await tx.mapVote.deleteMany({ where: { matchId } })
|
||||||
await tx.playerStats.deleteMany({ where: { matchId } })
|
await tx.playerStats.deleteMany({ where: { matchId } })
|
||||||
await tx.matchPlayer.deleteMany({ where: { matchId } })
|
await tx.matchPlayer.deleteMany({ where: { matchId } })
|
||||||
await tx.rankHistory.deleteMany({ where: { matchId } })
|
await tx.rankHistory.deleteMany({ where: { matchId } })
|
||||||
|
|||||||
191
src/app/api/matches/[matchId]/mapvote/admin-edit/route.ts
Normal file
191
src/app/api/matches/[matchId]/mapvote/admin-edit/route.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
// /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'
|
||||||
|
import { prisma } from '@/app/lib/prisma'
|
||||||
|
import { MapVoteAction } from '@/generated/prisma'
|
||||||
|
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||||
|
import { randomInt } from 'crypto'
|
||||||
|
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
|
||||||
|
|
||||||
|
/** -------- helpers copied (light) from main vote route -------- */
|
||||||
|
|
||||||
|
const ACTION_MAP: Record<MapVoteAction, 'ban'|'pick'|'decider'> = {
|
||||||
|
BAN: 'ban', PICK: 'pick', DECIDER: 'decider',
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapActionToApi(a: MapVoteAction): 'ban'|'pick'|'decider' {
|
||||||
|
return ACTION_MAP[a]
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSteps(bestOf: number, teamAId: string, teamBId: string) {
|
||||||
|
if (bestOf === 3) {
|
||||||
|
return [
|
||||||
|
{ order: 0, action: 'BAN', teamId: teamAId },
|
||||||
|
{ order: 1, action: 'BAN', teamId: teamBId },
|
||||||
|
{ order: 2, action: 'PICK', teamId: teamAId },
|
||||||
|
{ order: 3, action: 'PICK', teamId: teamBId },
|
||||||
|
{ order: 4, action: 'BAN', teamId: teamAId },
|
||||||
|
{ order: 5, action: 'BAN', teamId: teamBId },
|
||||||
|
{ order: 6, action: 'DECIDER', teamId: null },
|
||||||
|
] as const
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ order: 0, action: 'BAN', teamId: teamAId },
|
||||||
|
{ order: 1, action: 'BAN', teamId: teamBId },
|
||||||
|
{ order: 2, action: 'PICK', teamId: teamAId },
|
||||||
|
{ order: 3, action: 'PICK', teamId: teamBId },
|
||||||
|
{ order: 4, action: 'PICK', teamId: teamAId },
|
||||||
|
{ order: 5, action: 'PICK', teamId: teamBId },
|
||||||
|
{ order: 6, action: 'PICK', teamId: teamAId },
|
||||||
|
] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureVote(matchId: string) {
|
||||||
|
const match = await prisma.match.findUnique({
|
||||||
|
where: { id: matchId },
|
||||||
|
include: {
|
||||||
|
teamA: true,
|
||||||
|
teamB: true,
|
||||||
|
mapVote: { include: { steps: true } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!match) return { match: null, vote: null }
|
||||||
|
|
||||||
|
if (match.mapVote) return { match, vote: match.mapVote }
|
||||||
|
|
||||||
|
const bestOf = match.bestOf ?? 3
|
||||||
|
const mapPool = MAP_OPTIONS.map(m => m.key)
|
||||||
|
const firstIsA = (typeof randomInt === 'function') ? randomInt(0, 2) === 0 : Math.random() < 0.5
|
||||||
|
const firstTeamId = firstIsA ? match.teamA!.id : match.teamB!.id
|
||||||
|
const secondTeamId = firstIsA ? match.teamB!.id : match.teamA!.id
|
||||||
|
|
||||||
|
const stepsDef = buildSteps(bestOf, firstTeamId, secondTeamId)
|
||||||
|
|
||||||
|
const created = await prisma.mapVote.create({
|
||||||
|
data: {
|
||||||
|
matchId: match.id,
|
||||||
|
bestOf,
|
||||||
|
mapPool,
|
||||||
|
currentIdx: 0,
|
||||||
|
locked: false,
|
||||||
|
steps: {
|
||||||
|
create: stepsDef.map(s => ({
|
||||||
|
order: s.order,
|
||||||
|
action: s.action as MapVoteAction,
|
||||||
|
teamId: s.teamId,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { steps: true },
|
||||||
|
})
|
||||||
|
return { match, vote: created }
|
||||||
|
}
|
||||||
|
|
||||||
|
function shapeAdminEdit(vote: any) {
|
||||||
|
return vote.adminEditingBy
|
||||||
|
? {
|
||||||
|
enabled: true,
|
||||||
|
by: vote.adminEditingBy as string,
|
||||||
|
since: vote.adminEditingSince ? new Date(vote.adminEditingSince).toISOString() : null,
|
||||||
|
}
|
||||||
|
: { enabled: false as const, by: null, since: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function shapeStateSlim(vote: any) {
|
||||||
|
return {
|
||||||
|
bestOf: vote.bestOf as number,
|
||||||
|
mapPool: vote.mapPool as string[],
|
||||||
|
currentIndex: vote.currentIdx as number,
|
||||||
|
locked: vote.locked as boolean,
|
||||||
|
adminEdit: shapeAdminEdit(vote),
|
||||||
|
steps: [...vote.steps].sort((a: any, b: any) => a.order - b.order).map((s: any) => ({
|
||||||
|
order : s.order,
|
||||||
|
action : mapActionToApi(s.action),
|
||||||
|
teamId : s.teamId,
|
||||||
|
map : s.map ?? null,
|
||||||
|
chosenAt: s.chosenAt ? s.chosenAt.toISOString() : null,
|
||||||
|
chosenBy: s.chosenBy ?? null,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------ POST (toggle admin edit) ------------------ */
|
||||||
|
// Body: { enabled: boolean, force?: boolean }
|
||||||
|
// - enabled=true: setzt adminEditingBy = me.steamId (falls schon anderer Admin aktiv -> 409, außer force)
|
||||||
|
// - enabled=false: cleart adminEditingBy, wenn ich der aktive Editor bin (oder force)
|
||||||
|
export async function POST(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||||
|
const session = await getServerSession(authOptions(req))
|
||||||
|
const me = session?.user as { steamId: string; isAdmin?: boolean } | undefined
|
||||||
|
if (!me?.steamId) return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||||
|
if (!me.isAdmin) return NextResponse.json({ message: 'Nur Admins' }, { status: 403 })
|
||||||
|
|
||||||
|
const matchId = params.matchId
|
||||||
|
if (!matchId) return NextResponse.json({ message: 'Missing id' }, { status: 400 })
|
||||||
|
|
||||||
|
let body: { enabled?: boolean; force?: boolean } = {}
|
||||||
|
try { body = await req.json() } catch {}
|
||||||
|
const enabled = !!body.enabled
|
||||||
|
const force = !!body.force
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { match, vote } = await ensureVote(matchId)
|
||||||
|
if (!match || !vote) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 })
|
||||||
|
|
||||||
|
const someoneElseActive = vote.adminEditingBy && vote.adminEditingBy !== me.steamId
|
||||||
|
|
||||||
|
// Konfliktbehandlung
|
||||||
|
if (enabled && someoneElseActive && !force) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
message: 'Bereits im Admin-Edit durch anderen Benutzer',
|
||||||
|
adminEdit: shapeAdminEdit(vote),
|
||||||
|
},
|
||||||
|
{ status: 409 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle speichern
|
||||||
|
const updated = await prisma.mapVote.update({
|
||||||
|
where: { id: vote.id },
|
||||||
|
data: enabled
|
||||||
|
? { adminEditingBy: me.steamId, adminEditingSince: new Date() }
|
||||||
|
: (force || vote.adminEditingBy === me.steamId
|
||||||
|
? { adminEditingBy: null, adminEditingSince: null }
|
||||||
|
: {} // nichts ändern, wenn jemand anderer aktiv ist und !force
|
||||||
|
),
|
||||||
|
include: { steps: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
// 🔔 gezieltes SSE-Event
|
||||||
|
await sendServerSSEMessage({
|
||||||
|
type: 'map-vote-admin-edit',
|
||||||
|
matchId,
|
||||||
|
payload: {
|
||||||
|
enabled: !!updated.adminEditingBy,
|
||||||
|
by: updated.adminEditingBy ?? null,
|
||||||
|
since: updated.adminEditingSince ?? null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ adminEdit: shapeAdminEdit(updated), state: shapeStateSlim(updated) })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[map-vote][POST admin-edit] error', e)
|
||||||
|
return NextResponse.json({ message: 'Toggle fehlgeschlagen' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------ GET (optional status) ------------------ */
|
||||||
|
// Praktisch, falls du den Status separat pollen willst.
|
||||||
|
export async function GET(_req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||||
|
const matchId = params.matchId
|
||||||
|
if (!matchId) return NextResponse.json({ message: 'Missing id' }, { status: 400 })
|
||||||
|
try {
|
||||||
|
const { match, vote } = await ensureVote(matchId)
|
||||||
|
if (!match || !vote) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 })
|
||||||
|
return NextResponse.json({ adminEdit: shapeAdminEdit(vote), state: shapeStateSlim(vote) })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[map-vote][GET admin-edit] error', e)
|
||||||
|
return NextResponse.json({ message: 'Fehler beim Laden' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,41 +3,43 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/app/lib/auth'
|
import { authOptions } from '@/app/lib/auth'
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/app/lib/prisma'
|
||||||
import { MapVetoAction } from '@/generated/prisma'
|
import { MapVoteAction } from '@/generated/prisma'
|
||||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||||
|
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
|
||||||
|
|
||||||
/** gleicher Pool wie in deiner mapvote-Route */
|
// ---- Pool aus MAP_OPTIONS ableiten (nur "de_*", ohne Sonderkarten) ----
|
||||||
const ACTIVE_DUTY: string[] = [
|
const MAP_POOL: string[] = MAP_OPTIONS
|
||||||
'de_inferno','de_mirage','de_nuke','de_overpass','de_vertigo','de_ancient','de_anubis',
|
.filter(m => m.key.startsWith('de_') && m.key !== 'lobby_mapvote')
|
||||||
]
|
.map(m => m.key)
|
||||||
|
|
||||||
/** identische Logik wie in deiner mapvote-Route */
|
// identisch zu mapvote-Route
|
||||||
function vetoOpensAt(match: { matchDate: Date | null, demoDate: Date | null }) {
|
function voteOpensAt(match: { matchDate: Date | null, demoDate: Date | null }) {
|
||||||
const base = match.matchDate ?? match.demoDate ?? new Date()
|
const base = match.matchDate ?? match.demoDate ?? new Date()
|
||||||
return new Date(base.getTime() - 60 * 60 * 1000) // 1h vorher
|
return new Date(base.getTime() - 60 * 60 * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSteps(bestOf: number, teamAId: string, teamBId: string) {
|
// buildSteps so umbauen, dass die Reihenfolge (Startteam) variabel ist
|
||||||
|
function buildSteps(bestOf: number, firstId: string, secondId: string) {
|
||||||
if (bestOf === 3) {
|
if (bestOf === 3) {
|
||||||
return [
|
return [
|
||||||
{ order: 0, action: MapVetoAction.BAN, teamId: teamAId },
|
{ order: 0, action: MapVoteAction.BAN, teamId: firstId },
|
||||||
{ order: 1, action: MapVetoAction.BAN, teamId: teamBId },
|
{ order: 1, action: MapVoteAction.BAN, teamId: secondId },
|
||||||
{ order: 2, action: MapVetoAction.PICK, teamId: teamAId },
|
{ order: 2, action: MapVoteAction.PICK, teamId: firstId },
|
||||||
{ order: 3, action: MapVetoAction.PICK, teamId: teamBId },
|
{ order: 3, action: MapVoteAction.PICK, teamId: secondId },
|
||||||
{ order: 4, action: MapVetoAction.BAN, teamId: teamAId },
|
{ order: 4, action: MapVoteAction.BAN, teamId: firstId },
|
||||||
{ order: 5, action: MapVetoAction.BAN, teamId: teamBId },
|
{ order: 5, action: MapVoteAction.BAN, teamId: secondId },
|
||||||
{ order: 6, action: MapVetoAction.DECIDER, teamId: null },
|
{ order: 6, action: MapVoteAction.DECIDER, teamId: null },
|
||||||
] as const
|
] as const
|
||||||
}
|
}
|
||||||
// BO5
|
// BO5
|
||||||
return [
|
return [
|
||||||
{ order: 0, action: MapVetoAction.BAN, teamId: teamAId },
|
{ order: 0, action: MapVoteAction.BAN, teamId: firstId },
|
||||||
{ order: 1, action: MapVetoAction.BAN, teamId: teamBId },
|
{ order: 1, action: MapVoteAction.BAN, teamId: secondId },
|
||||||
{ order: 2, action: MapVetoAction.PICK, teamId: teamAId },
|
{ order: 2, action: MapVoteAction.PICK, teamId: firstId },
|
||||||
{ order: 3, action: MapVetoAction.PICK, teamId: teamBId },
|
{ order: 3, action: MapVoteAction.PICK, teamId: secondId },
|
||||||
{ order: 4, action: MapVetoAction.PICK, teamId: teamAId },
|
{ order: 4, action: MapVoteAction.PICK, teamId: firstId },
|
||||||
{ order: 5, action: MapVetoAction.PICK, teamId: teamBId },
|
{ order: 5, action: MapVoteAction.PICK, teamId: secondId },
|
||||||
{ order: 6, action: MapVetoAction.PICK, teamId: teamAId },
|
{ order: 6, action: MapVoteAction.PICK, teamId: firstId },
|
||||||
] as const
|
] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +52,6 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
const matchId = params.matchId
|
const matchId = params.matchId
|
||||||
if (!matchId) return NextResponse.json({ message: 'Missing matchId' }, { status: 400 })
|
if (!matchId) return NextResponse.json({ message: 'Missing matchId' }, { status: 400 })
|
||||||
|
|
||||||
// Match laden (inkl. Teams & BestOf für Steps)
|
|
||||||
const match = await prisma.match.findUnique({
|
const match = await prisma.match.findUnique({
|
||||||
where: { id: matchId },
|
where: { id: matchId },
|
||||||
select: {
|
select: {
|
||||||
@ -60,29 +61,32 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
demoDate: true,
|
demoDate: true,
|
||||||
teamA: { select: { id: true } },
|
teamA: { select: { id: true } },
|
||||||
teamB: { select: { id: true } },
|
teamB: { select: { id: true } },
|
||||||
mapVeto: { select: { id: true } },
|
mapVote: { select: { id: true } },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!match || !match.teamA?.id || !match.teamB?.id) {
|
if (!match?.teamA?.id || !match?.teamB?.id) {
|
||||||
return NextResponse.json({ message: 'Match/Teams nicht gefunden' }, { status: 404 })
|
return NextResponse.json({ message: 'Match/Teams nicht gefunden' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const bestOf = match.bestOf ?? 3
|
const bestOf = match.bestOf ?? 3
|
||||||
const stepsDef = buildSteps(bestOf, match.teamA.id, match.teamB.id)
|
// ---- Zufälliges Startteam bestimmen ----
|
||||||
const opensAt = vetoOpensAt({ matchDate: match.matchDate ?? null, demoDate: match.demoDate ?? null })
|
const firstId = Math.random() < 0.5 ? match.teamA.id : match.teamB.id
|
||||||
|
const secondId = firstId === match.teamA.id ? match.teamB.id : match.teamA.id
|
||||||
|
const stepsDef = buildSteps(bestOf, firstId, secondId)
|
||||||
|
|
||||||
|
const opensAt = voteOpensAt({ matchDate: match.matchDate ?? null, demoDate: match.demoDate ?? null })
|
||||||
|
|
||||||
// Reset in einer TX: alte Steps -> löschen, MapVeto -> löschen, neu anlegen
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
if (match.mapVeto?.id) {
|
if (match.mapVote?.id) {
|
||||||
await tx.mapVetoStep.deleteMany({ where: { vetoId: match.mapVeto.id } })
|
await tx.mapVoteStep.deleteMany({ where: { voteId: match.mapVote.id } })
|
||||||
await tx.mapVeto.delete({ where: { matchId } })
|
await tx.mapVote.delete({ where: { matchId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.mapVeto.create({
|
await tx.mapVote.create({
|
||||||
data: {
|
data: {
|
||||||
matchId,
|
matchId,
|
||||||
bestOf,
|
bestOf,
|
||||||
mapPool: ACTIVE_DUTY,
|
mapPool: MAP_POOL, // <- aus MAP_OPTIONS
|
||||||
currentIdx: 0,
|
currentIdx: 0,
|
||||||
locked: false,
|
locked: false,
|
||||||
opensAt,
|
opensAt,
|
||||||
@ -97,8 +101,6 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 🔔 UI-Refresh für alle Clients
|
|
||||||
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||||
|
|
||||||
return NextResponse.json({ ok: true })
|
return NextResponse.json({ ok: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,27 +3,37 @@ import { NextResponse, NextRequest } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/app/lib/auth'
|
import { authOptions } from '@/app/lib/auth'
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/app/lib/prisma'
|
||||||
import { MapVetoAction } from '@/generated/prisma'
|
import { MapVoteAction } from '@/generated/prisma'
|
||||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||||
|
import { randomInt } from 'crypto'
|
||||||
|
import { MAP_OPTIONS } from '@/app/lib/mapOptions'
|
||||||
|
import { createHash } from 'crypto'
|
||||||
|
|
||||||
/* -------------------- Konstanten -------------------- */
|
/* -------------------- Konstanten -------------------- */
|
||||||
|
|
||||||
const ACTIVE_DUTY: string[] = [
|
const ACTION_MAP: Record<MapVoteAction, 'ban'|'pick'|'decider'> = {
|
||||||
'de_inferno','de_mirage','de_nuke','de_overpass','de_vertigo','de_ancient','de_anubis',
|
|
||||||
]
|
|
||||||
|
|
||||||
const ACTION_MAP: Record<MapVetoAction, 'ban'|'pick'|'decider'> = {
|
|
||||||
BAN: 'ban', PICK: 'pick', DECIDER: 'decider',
|
BAN: 'ban', PICK: 'pick', DECIDER: 'decider',
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------- Helper -------------------- */
|
/* -------------------- Helper -------------------- */
|
||||||
|
|
||||||
function vetoOpensAt(match: { matchDate: Date | null, demoDate: Date | null }) {
|
// Admin-Edit-Flag setzen/zurücksetzen
|
||||||
|
async function setAdminEdit(voteId: string, by: string | null) {
|
||||||
|
return prisma.mapVote.update({
|
||||||
|
where: { id: voteId },
|
||||||
|
data: by
|
||||||
|
? { adminEditingBy: by, adminEditingSince: new Date() }
|
||||||
|
: { adminEditingBy: null, adminEditingSince: null },
|
||||||
|
include: { steps: true },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function voteOpensAt(match: { matchDate: Date | null, demoDate: Date | null }) {
|
||||||
const base = match.matchDate ?? match.demoDate ?? new Date()
|
const base = match.matchDate ?? match.demoDate ?? new Date()
|
||||||
return new Date(base.getTime() - 60 * 60 * 1000) // 1h vorher
|
return new Date(base.getTime() - 60 * 60 * 1000) // 1h vorher
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapActionToApi(a: MapVetoAction): 'ban'|'pick'|'decider' {
|
function mapActionToApi(a: MapVoteAction): 'ban'|'pick'|'decider' {
|
||||||
return ACTION_MAP[a]
|
return ACTION_MAP[a]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,8 +62,8 @@ function buildSteps(bestOf: number, teamAId: string, teamBId: string) {
|
|||||||
] as const
|
] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
function shapeState(veto: any) {
|
function shapeState(vote: any) {
|
||||||
const steps = [...veto.steps]
|
const steps = [...vote.steps]
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
.map((s: any) => ({
|
.map((s: any) => ({
|
||||||
order : s.order,
|
order : s.order,
|
||||||
@ -65,12 +75,20 @@ function shapeState(veto: any) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bestOf : veto.bestOf,
|
bestOf : vote.bestOf,
|
||||||
mapPool : veto.mapPool as string[],
|
mapPool : vote.mapPool as string[],
|
||||||
currentIndex: veto.currentIdx,
|
currentIndex: vote.currentIdx,
|
||||||
locked : veto.locked as boolean,
|
locked : vote.locked as boolean,
|
||||||
opensAt : veto.opensAt ? new Date(veto.opensAt).toISOString() : null,
|
opensAt : vote.opensAt ? new Date(vote.opensAt).toISOString() : null,
|
||||||
steps,
|
steps,
|
||||||
|
// Admin-Edit Shape
|
||||||
|
adminEdit: vote.adminEditingBy
|
||||||
|
? {
|
||||||
|
enabled: true,
|
||||||
|
by: vote.adminEditingBy as string,
|
||||||
|
since: vote.adminEditingSince ? new Date(vote.adminEditingSince).toISOString() : null,
|
||||||
|
}
|
||||||
|
: { enabled: false, by: null, since: null },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +120,6 @@ function shapePlayer(p: any) {
|
|||||||
|
|
||||||
// Base-URL aus Request ableiten (lokal/proxy-fähig)
|
// Base-URL aus Request ableiten (lokal/proxy-fähig)
|
||||||
function getBaseUrl(req: NextRequest | NextResponse) {
|
function getBaseUrl(req: NextRequest | NextResponse) {
|
||||||
// NextRequest hat headers; bei internen Aufrufen ggf. NextResponse, hier aber nur Request relevant
|
|
||||||
const proto = (req.headers.get('x-forwarded-proto') || 'http').split(',')[0].trim()
|
const proto = (req.headers.get('x-forwarded-proto') || 'http').split(',')[0].trim()
|
||||||
const host = (req.headers.get('x-forwarded-host') || req.headers.get('host') || '').split(',')[0].trim()
|
const host = (req.headers.get('x-forwarded-host') || req.headers.get('host') || '').split(',')[0].trim()
|
||||||
return `${proto}://${host}`
|
return `${proto}://${host}`
|
||||||
@ -115,10 +132,8 @@ async function fetchTeamApi(teamId: string | null | undefined, req: NextRequest)
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch(url, {
|
const r = await fetch(url, {
|
||||||
// interne Server-Fetches dürfen nicht gecacht werden
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
headers: {
|
headers: {
|
||||||
// Forward auth/proxy headers, falls nötig (nicht zwingend)
|
|
||||||
'x-forwarded-proto': req.headers.get('x-forwarded-proto') || '',
|
'x-forwarded-proto': req.headers.get('x-forwarded-proto') || '',
|
||||||
'x-forwarded-host' : req.headers.get('x-forwarded-host') || '',
|
'x-forwarded-host' : req.headers.get('x-forwarded-host') || '',
|
||||||
}
|
}
|
||||||
@ -129,7 +144,7 @@ async function fetchTeamApi(teamId: string | null | undefined, req: NextRequest)
|
|||||||
id: string
|
id: string
|
||||||
name?: string | null
|
name?: string | null
|
||||||
logo?: string | null
|
logo?: string | null
|
||||||
leader?: string | null // LeaderId
|
leader?: string | null
|
||||||
activePlayers: any[]
|
activePlayers: any[]
|
||||||
inactivePlayers: any[]
|
inactivePlayers: any[]
|
||||||
invitedPlayers: any[]
|
invitedPlayers: any[]
|
||||||
@ -139,7 +154,35 @@ async function fetchTeamApi(teamId: string | null | undefined, req: NextRequest)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leader bevorzugt aus Match-Relation; Fallback über Team-API (LeaderId -> Player aus Listen)
|
// Teams-Payload (mit Spielern) zusammenbauen
|
||||||
|
async function buildTeamsPayload(match: any, req: NextRequest) {
|
||||||
|
const [teamAApi, teamBApi] = await Promise.all([
|
||||||
|
fetchTeamApi(match.teamA?.id, req),
|
||||||
|
fetchTeamApi(match.teamB?.id, req),
|
||||||
|
])
|
||||||
|
|
||||||
|
const teamAPlayers = (teamAApi?.activePlayers ?? []).map(shapePlayer).filter(Boolean)
|
||||||
|
const teamBPlayers = (teamBApi?.activePlayers ?? []).map(shapePlayer).filter(Boolean)
|
||||||
|
|
||||||
|
return {
|
||||||
|
teamA: {
|
||||||
|
id : match.teamA?.id ?? null,
|
||||||
|
name : match.teamA?.name ?? null,
|
||||||
|
logo : match.teamA?.logo ?? null,
|
||||||
|
leader : resolveLeaderPlayer(match.teamA, teamAApi),
|
||||||
|
players: teamAPlayers,
|
||||||
|
},
|
||||||
|
teamB: {
|
||||||
|
id : match.teamB?.id ?? null,
|
||||||
|
name : match.teamB?.name ?? null,
|
||||||
|
logo : match.teamB?.logo ?? null,
|
||||||
|
leader : resolveLeaderPlayer(match.teamB, teamBApi),
|
||||||
|
players: teamBPlayers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leader bevorzugt aus Match-Relation; Fallback über Team-API
|
||||||
function resolveLeaderPlayer(matchTeam: any | null | undefined, teamApi: any | null) {
|
function resolveLeaderPlayer(matchTeam: any | null | undefined, teamApi: any | null) {
|
||||||
const leaderFromMatch = shapeLeader(matchTeam?.leader ?? null)
|
const leaderFromMatch = shapeLeader(matchTeam?.leader ?? null)
|
||||||
if (leaderFromMatch) return leaderFromMatch
|
if (leaderFromMatch) return leaderFromMatch
|
||||||
@ -156,13 +199,12 @@ function resolveLeaderPlayer(matchTeam: any | null | undefined, teamApi: any | n
|
|||||||
return shapePlayer(found) ?? { steamId: leaderId, name: '', avatar: '' }
|
return shapePlayer(found) ?? { steamId: leaderId, name: '', avatar: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureVeto(matchId: string) {
|
async function ensureVote(matchId: string) {
|
||||||
const match = await prisma.match.findUnique({
|
const match = await prisma.match.findUnique({
|
||||||
where: { id: matchId },
|
where: { id: matchId },
|
||||||
include: {
|
include: {
|
||||||
teamA : {
|
teamA : {
|
||||||
include: {
|
include: {
|
||||||
// Leader-Relation als Objekt laden
|
|
||||||
leader: {
|
leader: {
|
||||||
select: {
|
select: {
|
||||||
steamId: true,
|
steamId: true,
|
||||||
@ -189,21 +231,25 @@ async function ensureVeto(matchId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mapVeto: { include: { steps: true } },
|
mapVote: { include: { steps: true } },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!match) return { match: null, veto: null }
|
if (!match) return { match: null, vote: null }
|
||||||
|
|
||||||
// Bereits vorhanden?
|
// Bereits vorhanden?
|
||||||
if (match.mapVeto) return { match, veto: match.mapVeto }
|
if (match.mapVote) return { match, vote: match.mapVote }
|
||||||
|
|
||||||
// Neu anlegen
|
// Neu anlegen
|
||||||
const bestOf = match.bestOf ?? 3
|
const bestOf = match.bestOf ?? 3
|
||||||
const mapPool = ACTIVE_DUTY
|
const mapPool = MAP_OPTIONS.map(m => m.key)
|
||||||
const opensAt = vetoOpensAt({ matchDate: match.matchDate ?? null, demoDate: match.demoDate ?? null })
|
const opensAt = voteOpensAt({ matchDate: match.matchDate ?? null, demoDate: match.demoDate ?? null })
|
||||||
const stepsDef = buildSteps(bestOf, match.teamA!.id, match.teamB!.id)
|
|
||||||
|
|
||||||
const created = await prisma.mapVeto.create({
|
const firstIsA = (typeof randomInt === 'function') ? randomInt(0, 2) === 0 : Math.random() < 0.5
|
||||||
|
const firstTeamId = firstIsA ? match.teamA!.id : match.teamB!.id
|
||||||
|
const secondTeamId = firstIsA ? match.teamB!.id : match.teamA!.id
|
||||||
|
const stepsDef = buildSteps(bestOf, firstTeamId, secondTeamId)
|
||||||
|
|
||||||
|
const created = await prisma.mapVote.create({
|
||||||
data: {
|
data: {
|
||||||
matchId : match.id,
|
matchId : match.id,
|
||||||
bestOf,
|
bestOf,
|
||||||
@ -214,7 +260,7 @@ async function ensureVeto(matchId: string) {
|
|||||||
steps : {
|
steps : {
|
||||||
create: stepsDef.map(s => ({
|
create: stepsDef.map(s => ({
|
||||||
order : s.order,
|
order : s.order,
|
||||||
action: s.action as MapVetoAction,
|
action: s.action as MapVoteAction,
|
||||||
teamId: s.teamId,
|
teamId: s.teamId,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@ -222,7 +268,7 @@ async function ensureVeto(matchId: string) {
|
|||||||
include: { steps: true },
|
include: { steps: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
return { match, veto: created }
|
return { match, vote: created }
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeAvailableMaps(mapPool: string[], steps: Array<{ map: string | null }>) {
|
function computeAvailableMaps(mapPool: string[], steps: Array<{ map: string | null }>) {
|
||||||
@ -230,32 +276,27 @@ function computeAvailableMaps(mapPool: string[], steps: Array<{ map: string | nu
|
|||||||
return mapPool.filter(m => !used.has(m))
|
return mapPool.filter(m => !used.has(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Teams-Payload (mit Spielern) zusammenbauen
|
/* ---------- Visuals: deterministisches zufälliges Bild pro Map & Match ---------- */
|
||||||
async function buildTeamsPayload(match: any, req: NextRequest) {
|
|
||||||
const [teamAApi, teamBApi] = await Promise.all([
|
|
||||||
fetchTeamApi(match.teamA?.id, req),
|
|
||||||
fetchTeamApi(match.teamB?.id, req),
|
|
||||||
])
|
|
||||||
|
|
||||||
const teamAPlayers = (teamAApi?.activePlayers ?? []).map(shapePlayer).filter(Boolean)
|
function buildMapVisuals(matchId: string, mapPool: string[]) {
|
||||||
const teamBPlayers = (teamBApi?.activePlayers ?? []).map(shapePlayer).filter(Boolean)
|
const visuals: Record<string, { label: string; bg: string; images?: string[] }> = {}
|
||||||
|
for (const key of mapPool) {
|
||||||
|
const opt = MAP_OPTIONS.find(o => o.key === key)
|
||||||
|
const label = opt?.label ?? key
|
||||||
|
const imgs = opt?.images ?? []
|
||||||
|
let bg = `/assets/img/maps/${key}/1.jpg`
|
||||||
|
|
||||||
return {
|
if (imgs.length > 0) {
|
||||||
teamA: {
|
// deterministischer Index auf Basis von matchId+key
|
||||||
id : match.teamA?.id ?? null,
|
const h = createHash('sha256').update(`${matchId}:${key}`).digest('hex')
|
||||||
name : match.teamA?.name ?? null,
|
const n = parseInt(h.slice(0, 8), 16) // 32-bit
|
||||||
logo : match.teamA?.logo ?? null,
|
const idx = n % imgs.length
|
||||||
leader: resolveLeaderPlayer(match.teamA, teamAApi),
|
bg = imgs[idx]
|
||||||
players: teamAPlayers,
|
}
|
||||||
},
|
|
||||||
teamB: {
|
visuals[key] = { label, bg } // images optional mitgeben: { label, bg, images: imgs }
|
||||||
id : match.teamB?.id ?? null,
|
|
||||||
name : match.teamB?.name ?? null,
|
|
||||||
logo : match.teamB?.logo ?? null,
|
|
||||||
leader: resolveLeaderPlayer(match.teamB, teamBApi),
|
|
||||||
players: teamBPlayers,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
return visuals
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------- GET -------------------- */
|
/* -------------------- GET -------------------- */
|
||||||
@ -265,13 +306,14 @@ export async function GET(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
const matchId = params.matchId
|
const matchId = params.matchId
|
||||||
if (!matchId) return NextResponse.json({ message: 'Missing id' }, { status: 400 })
|
if (!matchId) return NextResponse.json({ message: 'Missing id' }, { status: 400 })
|
||||||
|
|
||||||
const { match, veto } = await ensureVeto(matchId)
|
const { match, vote } = await ensureVote(matchId)
|
||||||
if (!match || !veto) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 })
|
if (!match || !vote) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 })
|
||||||
|
|
||||||
const teams = await buildTeamsPayload(match, req)
|
const teams = await buildTeamsPayload(match, req)
|
||||||
|
const mapVisuals = buildMapVisuals(match.id, vote.mapPool)
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ ...shapeState(veto), teams },
|
{ ...shapeState(vote), mapVisuals, teams },
|
||||||
{ headers: { 'Cache-Control': 'no-store' } },
|
{ headers: { 'Cache-Control': 'no-store' } },
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -290,43 +332,61 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
const matchId = params.matchId
|
const matchId = params.matchId
|
||||||
if (!matchId) return NextResponse.json({ message: 'Missing id' }, { status: 400 })
|
if (!matchId) return NextResponse.json({ message: 'Missing id' }, { status: 400 })
|
||||||
|
|
||||||
let body: { map?: string } = {}
|
type ToggleBody = { map?: string; adminEdit?: boolean }
|
||||||
|
let body: ToggleBody = {}
|
||||||
try { body = await req.json() } catch {}
|
try { body = await req.json() } catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { match, veto } = await ensureVeto(matchId)
|
const { match, vote } = await ensureVote(matchId)
|
||||||
if (!match || !veto) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 })
|
if (!match || !vote) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 })
|
||||||
|
|
||||||
// Öffnungsfenster (1h vor Match-/Demo-Beginn)
|
/* -------- Admin-Edit umschalten (früher Exit) -------- */
|
||||||
const opensAt = veto.opensAt ?? vetoOpensAt({ matchDate: match.matchDate ?? null, demoDate: match.demoDate ?? null })
|
if (typeof body.adminEdit === 'boolean') {
|
||||||
const isOpen = new Date() >= new Date(opensAt)
|
if (!me.isAdmin) {
|
||||||
if (!isOpen && !me.isAdmin) return NextResponse.json({ message: 'Veto ist noch nicht offen' }, { status: 403 })
|
return NextResponse.json({ message: 'Nur Admins dürfen den Edit-Mode setzen' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
// Schon abgeschlossen?
|
const updated = await setAdminEdit(vote.id, body.adminEdit ? me.steamId : null)
|
||||||
if (veto.locked) return NextResponse.json({ message: 'Veto bereits abgeschlossen' }, { status: 409 })
|
|
||||||
|
|
||||||
// Aktuellen Schritt bestimmen
|
|
||||||
const stepsSorted = [...veto.steps].sort((a: any, b: any) => a.order - b.order)
|
|
||||||
const current = stepsSorted.find((s: any) => s.order === veto.currentIdx)
|
|
||||||
|
|
||||||
if (!current) {
|
|
||||||
// Kein Schritt mehr -> Veto abschließen
|
|
||||||
await prisma.mapVeto.update({ where: { id: veto.id }, data: { locked: true } })
|
|
||||||
|
|
||||||
const updated = await prisma.mapVeto.findUnique({
|
|
||||||
where: { id: veto.id },
|
|
||||||
include: { steps: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
// 🔔 Broadcast (flat)
|
|
||||||
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||||
|
|
||||||
const teams = await buildTeamsPayload(match, req)
|
const teams = await buildTeamsPayload(match, req)
|
||||||
|
const mapVisuals = buildMapVisuals(match.id, updated.mapPool)
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated), teams })
|
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||||
}
|
}
|
||||||
|
|
||||||
const available = computeAvailableMaps(veto.mapPool, stepsSorted)
|
/* -------- Wenn anderer Admin editiert: Voting sperren -------- */
|
||||||
|
if (vote.adminEditingBy && vote.adminEditingBy !== me.steamId) {
|
||||||
|
return NextResponse.json({ message: 'Admin-Edit aktiv – Voting vorübergehend deaktiviert' }, { status: 423 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- Zeitfenster prüfen (Admins dürfen trotzdem) -------- */
|
||||||
|
const opensAt = vote.opensAt ?? voteOpensAt({ matchDate: match.matchDate ?? null, demoDate: match.demoDate ?? null })
|
||||||
|
const isOpen = new Date() >= new Date(opensAt)
|
||||||
|
if (!isOpen && !me.isAdmin) return NextResponse.json({ message: 'Voting ist noch nicht offen' }, { status: 403 })
|
||||||
|
|
||||||
|
// Schon abgeschlossen?
|
||||||
|
if (vote.locked) return NextResponse.json({ message: 'Voting bereits abgeschlossen' }, { status: 409 })
|
||||||
|
|
||||||
|
// Aktuellen Schritt bestimmen
|
||||||
|
const stepsSorted = [...vote.steps].sort((a: any, b: any) => a.order - b.order)
|
||||||
|
const current = stepsSorted.find((s: any) => s.order === vote.currentIdx)
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
// Kein Schritt mehr -> Vote abschließen
|
||||||
|
await prisma.mapVote.update({ where: { id: vote.id }, data: { locked: true } })
|
||||||
|
const updated = await prisma.mapVote.findUnique({ where: { id: vote.id }, include: { steps: true } })
|
||||||
|
|
||||||
|
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||||
|
|
||||||
|
const teams = await buildTeamsPayload(match, req)
|
||||||
|
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||||
|
|
||||||
|
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||||
|
}
|
||||||
|
|
||||||
|
const available = computeAvailableMaps(vote.mapPool, stepsSorted)
|
||||||
|
|
||||||
// DECIDER automatisch setzen, wenn nur noch 1 Map übrig
|
// DECIDER automatisch setzen, wenn nur noch 1 Map übrig
|
||||||
if (current.action === 'DECIDER') {
|
if (current.action === 'DECIDER') {
|
||||||
@ -335,30 +395,30 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
}
|
}
|
||||||
const lastMap = available[0]
|
const lastMap = available[0]
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
await tx.mapVetoStep.update({
|
await tx.mapVoteStep.update({
|
||||||
where: { id: current.id },
|
where: { id: current.id },
|
||||||
data : { map: lastMap, chosenAt: new Date(), chosenBy: me.steamId },
|
data : { map: lastMap, chosenAt: new Date(), chosenBy: me.steamId },
|
||||||
})
|
})
|
||||||
await tx.mapVeto.update({
|
await tx.mapVote.update({
|
||||||
where: { id: veto.id },
|
where: { id: vote.id },
|
||||||
data : { currentIdx: veto.currentIdx + 1, locked: true },
|
data : { currentIdx: vote.currentIdx + 1, locked: true },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const updated = await prisma.mapVeto.findUnique({
|
const updated = await prisma.mapVote.findUnique({
|
||||||
where: { id: veto.id },
|
where: { id: vote.id },
|
||||||
include: { steps: true },
|
include: { steps: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
// 🔔 Broadcast (flat)
|
|
||||||
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||||
|
|
||||||
const teams = await buildTeamsPayload(match, req)
|
const teams = await buildTeamsPayload(match, req)
|
||||||
|
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated), teams })
|
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rechte prüfen (Admin oder Leader des Teams am Zug) – weiterhin via leaderId
|
// Rechte prüfen (Admin oder Leader des Teams am Zug)
|
||||||
const isLeaderA = !!(match as any).teamA?.leaderId && (match as any).teamA.leaderId === me.steamId
|
const isLeaderA = !!(match as any).teamA?.leaderId && (match as any).teamA.leaderId === me.steamId
|
||||||
const isLeaderB = !!(match as any).teamB?.leaderId && (match as any).teamB.leaderId === me.steamId
|
const isLeaderB = !!(match as any).teamB?.leaderId && (match as any).teamB.leaderId === me.steamId
|
||||||
const allowed = me.isAdmin || (current.teamId && (
|
const allowed = me.isAdmin || (current.teamId && (
|
||||||
@ -370,20 +430,20 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
// Payload validieren
|
// Payload validieren
|
||||||
const map = body.map?.trim()
|
const map = body.map?.trim()
|
||||||
if (!map) return NextResponse.json({ message: 'map fehlt' }, { status: 400 })
|
if (!map) return NextResponse.json({ message: 'map fehlt' }, { status: 400 })
|
||||||
if (!veto.mapPool.includes(map)) return NextResponse.json({ message: 'Map nicht im Pool' }, { status: 400 })
|
if (!vote.mapPool.includes(map)) return NextResponse.json({ message: 'Map nicht im Pool' }, { status: 400 })
|
||||||
if (!available.includes(map)) return NextResponse.json({ message: 'Map bereits vergeben' }, { status: 409 })
|
if (!available.includes(map)) return NextResponse.json({ message: 'Map bereits vergeben' }, { status: 409 })
|
||||||
|
|
||||||
// Schritt setzen & ggf. weiterdrehen (+ Decider evtl. auto)
|
// Schritt setzen & ggf. weiterdrehen (+ Decider evtl. auto)
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
// aktuellen Schritt setzen
|
// aktuellen Schritt setzen
|
||||||
await tx.mapVetoStep.update({
|
await tx.mapVoteStep.update({
|
||||||
where: { id: current.id },
|
where: { id: current.id },
|
||||||
data : { map, chosenAt: new Date(), chosenBy: me.steamId },
|
data : { map, chosenAt: new Date(), chosenBy: me.steamId },
|
||||||
})
|
})
|
||||||
|
|
||||||
// neuen Zustand ermitteln
|
// neuen Zustand ermitteln
|
||||||
const after = await tx.mapVeto.findUnique({
|
const after = await tx.mapVote.findUnique({
|
||||||
where : { id: veto.id },
|
where : { id: vote.id },
|
||||||
include: { steps: true },
|
include: { steps: true },
|
||||||
})
|
})
|
||||||
if (!after) return
|
if (!after) return
|
||||||
@ -397,7 +457,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
if (next?.action === 'DECIDER') {
|
if (next?.action === 'DECIDER') {
|
||||||
const avail = computeAvailableMaps(after.mapPool, stepsAfter)
|
const avail = computeAvailableMaps(after.mapPool, stepsAfter)
|
||||||
if (avail.length === 1) {
|
if (avail.length === 1) {
|
||||||
await tx.mapVetoStep.update({
|
await tx.mapVoteStep.update({
|
||||||
where: { id: next.id },
|
where: { id: next.id },
|
||||||
data : { map: avail[0], chosenAt: new Date(), chosenBy: me.steamId },
|
data : { map: avail[0], chosenAt: new Date(), chosenBy: me.steamId },
|
||||||
})
|
})
|
||||||
@ -410,23 +470,23 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
const maxOrder = Math.max(...stepsAfter.map(s => s.order))
|
const maxOrder = Math.max(...stepsAfter.map(s => s.order))
|
||||||
if (idx > maxOrder) locked = true
|
if (idx > maxOrder) locked = true
|
||||||
|
|
||||||
await tx.mapVeto.update({
|
await tx.mapVote.update({
|
||||||
where: { id: after.id },
|
where: { id: after.id },
|
||||||
data : { currentIdx: idx, locked },
|
data : { currentIdx: idx, locked },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const updated = await prisma.mapVeto.findUnique({
|
const updated = await prisma.mapVote.findUnique({
|
||||||
where : { id: veto.id },
|
where : { id: vote.id },
|
||||||
include: { steps: true },
|
include: { steps: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
// 🔔 Broadcast (flat)
|
|
||||||
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||||
|
|
||||||
const teams = await buildTeamsPayload(match, req)
|
const teams = await buildTeamsPayload(match, req)
|
||||||
|
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated), teams })
|
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[map-vote][POST] error', e)
|
console.error('[map-vote][POST] error', e)
|
||||||
return NextResponse.json({ message: 'Aktion fehlgeschlagen' }, { status: 500 })
|
return NextResponse.json({ message: 'Aktion fehlgeschlagen' }, { status: 500 })
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
if (!me?.steamId) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
if (!me?.steamId) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||||
|
|
||||||
const body = await req.json().catch(() => ({}))
|
const body = await req.json().catch(() => ({}))
|
||||||
const { title, matchType, teamAId, teamBId, matchDate, map, vetoLeadMinutes } = body ?? {}
|
const { title, matchType, teamAId, teamBId, matchDate, map, voteLeadMinutes } = body ?? {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const match = await prisma.match.findUnique({
|
const match = await prisma.match.findUnique({
|
||||||
@ -20,7 +20,7 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
include: {
|
include: {
|
||||||
teamA: { include: { leader: true } },
|
teamA: { include: { leader: true } },
|
||||||
teamB: { include: { leader: true } },
|
teamB: { include: { leader: true } },
|
||||||
mapVeto: true,
|
mapVote: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!match) return NextResponse.json({ error: 'Match not found' }, { status: 404 })
|
if (!match) return NextResponse.json({ error: 'Match not found' }, { status: 404 })
|
||||||
@ -42,7 +42,7 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
updateData.matchDate = matchDate ? new Date(matchDate) : null
|
updateData.matchDate = matchDate ? new Date(matchDate) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const lead = Number.isFinite(Number(vetoLeadMinutes)) ? Number(vetoLeadMinutes) : 60
|
const lead = Number.isFinite(Number(voteLeadMinutes)) ? Number(voteLeadMinutes) : 60
|
||||||
let opensAt: Date | null = null
|
let opensAt: Date | null = null
|
||||||
if (updateData.matchDate instanceof Date) {
|
if (updateData.matchDate instanceof Date) {
|
||||||
opensAt = new Date(updateData.matchDate.getTime() - lead * 60 * 1000)
|
opensAt = new Date(updateData.matchDate.getTime() - lead * 60 * 1000)
|
||||||
@ -54,12 +54,12 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
const m = await tx.match.update({
|
const m = await tx.match.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: updateData,
|
data: updateData,
|
||||||
include: { mapVeto: true },
|
include: { mapVote: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (opensAt) {
|
if (opensAt) {
|
||||||
if (!m.mapVeto) {
|
if (!m.mapVote) {
|
||||||
await tx.mapVeto.create({
|
await tx.mapVote.create({
|
||||||
data: {
|
data: {
|
||||||
matchId: m.id,
|
matchId: m.id,
|
||||||
opensAt,
|
opensAt,
|
||||||
@ -67,8 +67,8 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await tx.mapVeto.update({
|
await tx.mapVote.update({
|
||||||
where: { id: m.mapVeto.id },
|
where: { id: m.mapVote.id },
|
||||||
data: { opensAt },
|
data: { opensAt },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -79,7 +79,7 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
include: {
|
include: {
|
||||||
teamA: { include: { leader: true } },
|
teamA: { include: { leader: true } },
|
||||||
teamB: { include: { leader: true } },
|
teamB: { include: { leader: true } },
|
||||||
mapVeto: true,
|
mapVote: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -93,7 +93,7 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({
|
||||||
type: 'map-vote-updated',
|
type: 'map-vote-updated',
|
||||||
payload: { matchId: updated.id, opensAt: updated.mapVeto?.opensAt ?? null },
|
payload: { matchId: updated.id, opensAt: updated.mapVote?.opensAt ?? null },
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@ -104,7 +104,7 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
teamBId: updated.teamBId,
|
teamBId: updated.teamBId,
|
||||||
matchDate: updated.matchDate,
|
matchDate: updated.matchDate,
|
||||||
map: updated.map,
|
map: updated.map,
|
||||||
mapVeto: updated.mapVeto,
|
mapVote: updated.mapVote,
|
||||||
}, { headers: { 'Cache-Control': 'no-store' } })
|
}, { headers: { 'Cache-Control': 'no-store' } })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`PUT /matches/${id}/meta failed:`, err)
|
console.error(`PUT /matches/${id}/meta failed:`, err)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/app/lib/auth'
|
import { authOptions } from '@/app/lib/auth'
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/app/lib/prisma'
|
||||||
import { MapVetoAction } from '@/generated/prisma'
|
import { MapVoteAction } from '@/generated/prisma'
|
||||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@ -12,24 +12,24 @@ export const dynamic = 'force-dynamic'
|
|||||||
function buildSteps(bestOf: number, teamAId: string, teamBId: string) {
|
function buildSteps(bestOf: number, teamAId: string, teamBId: string) {
|
||||||
if (bestOf === 5) {
|
if (bestOf === 5) {
|
||||||
return [
|
return [
|
||||||
{ order: 0, action: MapVetoAction.BAN, teamId: teamAId },
|
{ order: 0, action: MapVoteAction.BAN, teamId: teamAId },
|
||||||
{ order: 1, action: MapVetoAction.BAN, teamId: teamBId },
|
{ order: 1, action: MapVoteAction.BAN, teamId: teamBId },
|
||||||
{ order: 2, action: MapVetoAction.PICK, teamId: teamAId },
|
{ order: 2, action: MapVoteAction.PICK, teamId: teamAId },
|
||||||
{ order: 3, action: MapVetoAction.PICK, teamId: teamBId },
|
{ order: 3, action: MapVoteAction.PICK, teamId: teamBId },
|
||||||
{ order: 4, action: MapVetoAction.PICK, teamId: teamAId },
|
{ order: 4, action: MapVoteAction.PICK, teamId: teamAId },
|
||||||
{ order: 5, action: MapVetoAction.PICK, teamId: teamBId },
|
{ order: 5, action: MapVoteAction.PICK, teamId: teamBId },
|
||||||
{ order: 6, action: MapVetoAction.PICK, teamId: teamAId },
|
{ order: 6, action: MapVoteAction.PICK, teamId: teamAId },
|
||||||
] as const
|
] as const
|
||||||
}
|
}
|
||||||
// default BO3
|
// default BO3
|
||||||
return [
|
return [
|
||||||
{ order: 0, action: MapVetoAction.BAN, teamId: teamAId },
|
{ order: 0, action: MapVoteAction.BAN, teamId: teamAId },
|
||||||
{ order: 1, action: MapVetoAction.BAN, teamId: teamBId },
|
{ order: 1, action: MapVoteAction.BAN, teamId: teamBId },
|
||||||
{ order: 2, action: MapVetoAction.PICK, teamId: teamAId },
|
{ order: 2, action: MapVoteAction.PICK, teamId: teamAId },
|
||||||
{ order: 3, action: MapVetoAction.PICK, teamId: teamBId },
|
{ order: 3, action: MapVoteAction.PICK, teamId: teamBId },
|
||||||
{ order: 4, action: MapVetoAction.BAN, teamId: teamAId },
|
{ order: 4, action: MapVoteAction.BAN, teamId: teamAId },
|
||||||
{ order: 5, action: MapVetoAction.BAN, teamId: teamBId },
|
{ order: 5, action: MapVoteAction.BAN, teamId: teamBId },
|
||||||
{ order: 6, action: MapVetoAction.DECIDER, teamId: null },
|
{ order: 6, action: MapVoteAction.DECIDER, teamId: null },
|
||||||
] as const
|
] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,12 +146,12 @@ export async function POST (req: NextRequest) {
|
|||||||
await tx.matchPlayer.createMany({ data: playersData, skipDuplicates: true })
|
await tx.matchPlayer.createMany({ data: playersData, skipDuplicates: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) MapVeto anlegen
|
// 6) MapVote anlegen
|
||||||
const baseDate = newMatch.demoDate ?? plannedAt
|
const baseDate = newMatch.demoDate ?? plannedAt
|
||||||
const opensAt = new Date(baseDate.getTime() - 60 * 60 * 1000)
|
const opensAt = new Date(baseDate.getTime() - 60 * 60 * 1000)
|
||||||
const stepsDef = buildSteps(bestOfInt, teamAId, teamBId)
|
const stepsDef = buildSteps(bestOfInt, teamAId, teamBId)
|
||||||
|
|
||||||
await tx.mapVeto.create({
|
await tx.mapVote.create({
|
||||||
data: {
|
data: {
|
||||||
matchId : newMatch.id,
|
matchId : newMatch.id,
|
||||||
bestOf : bestOfInt,
|
bestOf : bestOfInt,
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export async function GET(req: Request) {
|
|||||||
teamA : true,
|
teamA : true,
|
||||||
teamB : true,
|
teamB : true,
|
||||||
players: { include: { user: true, stats: true, team: true } },
|
players: { include: { user: true, stats: true, team: true } },
|
||||||
mapVeto: { include: { steps: true } },
|
mapVote: { include: { steps: true } },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -27,13 +27,13 @@ export async function GET(req: Request) {
|
|||||||
let totalSteps: number | null = null
|
let totalSteps: number | null = null
|
||||||
let opensInMinutes: number | null = null // <-- optional
|
let opensInMinutes: number | null = null // <-- optional
|
||||||
|
|
||||||
if (m.mapVeto) {
|
if (m.mapVote) {
|
||||||
const stepsSorted = [...m.mapVeto.steps].sort((a, b) => a.order - b.order)
|
const stepsSorted = [...m.mapVote.steps].sort((a, b) => a.order - b.order)
|
||||||
const anyChosen = stepsSorted.some(s => !!s.chosenAt)
|
const anyChosen = stepsSorted.some(s => !!s.chosenAt)
|
||||||
status = m.mapVeto.locked ? 'completed' : (anyChosen ? 'in_progress' : 'not_started')
|
status = m.mapVote.locked ? 'completed' : (anyChosen ? 'in_progress' : 'not_started')
|
||||||
|
|
||||||
const computedOpensAt =
|
const computedOpensAt =
|
||||||
m.mapVeto.opensAt ??
|
m.mapVote.opensAt ??
|
||||||
(() => {
|
(() => {
|
||||||
const base = m.matchDate ?? m.demoDate ?? new Date()
|
const base = m.matchDate ?? m.demoDate ?? new Date()
|
||||||
return new Date(base.getTime() - 60 * 60 * 1000) // 1h vorher
|
return new Date(base.getTime() - 60 * 60 * 1000) // 1h vorher
|
||||||
@ -47,8 +47,8 @@ export async function GET(req: Request) {
|
|||||||
opensInMinutes = Math.max(0, Math.ceil((oa - now) / 60000))
|
opensInMinutes = Math.max(0, Math.ceil((oa - now) / 60000))
|
||||||
}
|
}
|
||||||
|
|
||||||
currentIndex = m.mapVeto.currentIdx
|
currentIndex = m.mapVote.currentIdx
|
||||||
const cur = stepsSorted.find(s => s.order === m.mapVeto?.currentIdx)
|
const cur = stepsSorted.find(s => s.order === m.mapVote?.currentIdx)
|
||||||
currentAction = (cur?.action as 'BAN'|'PICK'|'DECIDER') ?? null
|
currentAction = (cur?.action as 'BAN'|'PICK'|'DECIDER') ?? null
|
||||||
decidedCount = stepsSorted.filter(s => !!s.chosenAt).length
|
decidedCount = stepsSorted.filter(s => !!s.chosenAt).length
|
||||||
totalSteps = stepsSorted.length
|
totalSteps = stepsSorted.length
|
||||||
@ -68,7 +68,7 @@ export async function GET(req: Request) {
|
|||||||
scoreB : m.scoreB,
|
scoreB : m.scoreB,
|
||||||
winnerTeam: m.winnerTeam ?? null,
|
winnerTeam: m.winnerTeam ?? null,
|
||||||
|
|
||||||
mapVeto: m.mapVeto ? {
|
mapVote: m.mapVote ? {
|
||||||
status,
|
status,
|
||||||
opensAt: opensAtISO,
|
opensAt: opensAtISO,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|||||||
@ -10,9 +10,9 @@ type ButtonProps = {
|
|||||||
modalId?: string
|
modalId?: string
|
||||||
color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent'
|
color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent'
|
||||||
variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link'
|
variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link'
|
||||||
size?: 'xs' |'sm' | 'md' | 'lg'
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
||||||
className?: string
|
className?: string
|
||||||
dropDirection?: "up" | "down" | "auto"
|
dropDirection?: 'up' | 'down' | 'auto'
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
} & ButtonHTMLAttributes<HTMLButtonElement>
|
} & ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|||||||
variant = 'solid',
|
variant = 'solid',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
className,
|
className,
|
||||||
dropDirection = "down",
|
dropDirection = 'down',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
@ -52,12 +52,14 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|||||||
sm: 'py-2 px-3',
|
sm: 'py-2 px-3',
|
||||||
md: 'py-3 px-4',
|
md: 'py-3 px-4',
|
||||||
lg: 'p-4 sm:p-5',
|
lg: 'p-4 sm:p-5',
|
||||||
|
xl: 'py-6 px-8 text-lg',
|
||||||
|
full: 'py-6 px-8 text-lg w-full',
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = `
|
const base = `
|
||||||
${sizeClasses[size] || sizeClasses['md']}
|
${sizeClasses[size] || sizeClasses['md']}
|
||||||
inline-flex items-center gap-x-2 text-sm font-medium rounded-lg
|
inline-flex items-center gap-x-2 text-sm font-medium rounded-lg
|
||||||
focus:outline-hidden disabled:opacity-50 disabled:pointer-events-none
|
focus:outline-hidden disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
`
|
`
|
||||||
|
|
||||||
const variants: Record<string, Record<string, string>> = {
|
const variants: Record<string, Record<string, string>> = {
|
||||||
@ -83,7 +85,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|||||||
gray: 'border border-transparent text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
gray: 'border border-transparent text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
||||||
teal: 'border border-transparent text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
teal: 'border border-transparent text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
||||||
green: 'border border-transparent text-green-600 hover:bg-green-100 hover:text-green-800 focus:bg-green-100 focus:text-green-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
green: 'border border-transparent text-green-600 hover:bg-green-100 hover:text-green-800 focus:bg-green-100 focus:text-green-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-white dark:focus:bg-neutral-700 dark:focus:text-white',
|
||||||
transparent: 'border border-transparent text-transparent-600 hover:bg-transparent-100 hover:text-transparent-800 focus:bg-transparent-100 focus:text-transparent-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',
|
||||||
},
|
},
|
||||||
soft: {
|
soft: {
|
||||||
blue: 'bg-blue-100 text-blue-800 hover:bg-blue-200 focus:bg-blue-200 dark:text-blue-400 dark:hover:bg-blue-900 dark:focus:bg-blue-900',
|
blue: 'bg-blue-100 text-blue-800 hover:bg-blue-200 focus:bg-blue-200 dark:text-blue-400 dark:hover:bg-blue-900 dark:focus:bg-blue-900',
|
||||||
@ -107,39 +109,50 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|||||||
gray: 'border border-transparent text-gray-600 hover:text-gray-800 focus:text-gray-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
gray: 'border border-transparent text-gray-600 hover:text-gray-800 focus:text-gray-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
||||||
teal: 'border border-transparent text-teal-600 hover:text-teal-800 focus:text-teal-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
teal: 'border border-transparent text-teal-600 hover:text-teal-800 focus:text-teal-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
||||||
green: 'border border-transparent text-green-600 hover:text-green-800 focus:text-green-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
green: 'border border-transparent text-green-600 hover:text-green-800 focus:text-green-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
||||||
transparent: 'border border-transparent text-transparent-600 hover:text-transparent-800 focus:text-transparent-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white'
|
transparent: 'border border-transparent text-transparent-600 hover:text-transparent-800 focus:text-transparent-800 dark:text-neutral-400 dark:hover:text-white dark:focus:text-white',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const variantClasses = variants[variant]?.[color] || variants.solid.blue
|
||||||
|
|
||||||
|
// Entfernt alle Hover/Focus/Active Tokens (inkl. dark:hover:..., sm:focus:..., etc.)
|
||||||
|
const stripInteractive = (cls: string) =>
|
||||||
|
cls
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(c => c && !c.includes('hover:') && !c.includes('focus:') && !c.includes('active:'))
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
const safeVariantClasses = disabled ? stripInteractive(variantClasses) : variantClasses
|
||||||
|
|
||||||
const classes = `
|
const classes = `
|
||||||
${base}
|
${base}
|
||||||
${variants[variant]?.[color] || variants.solid.blue}
|
${safeVariantClasses}
|
||||||
${className || ''}
|
${className || ''}
|
||||||
`
|
`
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && dropDirection === "auto" && buttonRef.current) {
|
if (open && dropDirection === 'auto' && buttonRef.current) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const rect = buttonRef.current!.getBoundingClientRect();
|
const rect = buttonRef.current!.getBoundingClientRect()
|
||||||
const dropdownHeight = 200;
|
const dropdownHeight = 200
|
||||||
const spaceBelow = window.innerHeight - rect.bottom;
|
const spaceBelow = window.innerHeight - rect.bottom
|
||||||
const spaceAbove = rect.top;
|
const spaceAbove = rect.top
|
||||||
|
|
||||||
if (spaceBelow < dropdownHeight && spaceAbove > dropdownHeight) {
|
if (spaceBelow < dropdownHeight && spaceAbove > dropdownHeight) {
|
||||||
setDirection("up");
|
setDirection('up')
|
||||||
} else {
|
} else {
|
||||||
setDirection("down");
|
setDirection('down')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}, [open, dropDirection]);
|
}, [open, dropDirection])
|
||||||
|
|
||||||
const toggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const toggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
const next = !open
|
const next = !open
|
||||||
setOpen(next)
|
setOpen(next)
|
||||||
onToggle?.(next)
|
onToggle?.(next)
|
||||||
onClick?.(event)
|
onClick?.(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -147,6 +160,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|||||||
type="button"
|
type="button"
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
|
disabled={disabled}
|
||||||
{...modalAttributes}
|
{...modalAttributes}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -247,7 +247,7 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
<div className="bg-yellow-300 dark:bg-yellow-500 text-center py-2 font-bold tracking-wider">
|
<div className="bg-yellow-300 dark:bg-yellow-500 text-center py-2 font-bold tracking-wider">
|
||||||
{dayLabel}<br />{dateKey}
|
{dayLabel}<br />{dateKey}
|
||||||
</div>
|
</div>
|
||||||
{dayMatches.map(m => {
|
{dayMatches.map((m: Match) => {
|
||||||
const started = new Date(m.demoDate).getTime() <= Date.now()
|
const started = new Date(m.demoDate).getTime() <= Date.now()
|
||||||
const unfinished = !m.winnerTeam && m.scoreA == null && m.scoreB == null
|
const unfinished = !m.winnerTeam && m.scoreA == null && m.scoreB == null
|
||||||
const isLive = started && unfinished
|
const isLive = started && unfinished
|
||||||
@ -273,23 +273,23 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Map-Veto Badge */}
|
{/* Map-Vote Badge */}
|
||||||
{m.mapVeto && (
|
{m.mapVote && (
|
||||||
<span
|
<span
|
||||||
className={`
|
className={`
|
||||||
px-2 py-0.5 rounded-full text-[11px] font-semibold
|
px-2 py-0.5 rounded-full text-[11px] font-semibold
|
||||||
${m.mapVeto.isOpen ? 'bg-green-300 dark:bg-green-600 text-white' : 'bg-neutral-200 dark:bg-neutral-700'}
|
${m.mapVote.isOpen ? 'bg-green-300 dark:bg-green-600 text-white' : 'bg-neutral-200 dark:bg-neutral-700'}
|
||||||
`}
|
`}
|
||||||
title={
|
title={
|
||||||
m.mapVeto.opensAt
|
m.mapVote.opensAt
|
||||||
? `Öffnet ${format(new Date(m.mapVeto.opensAt), 'dd.MM.yyyy HH:mm', { locale: de })} Uhr`
|
? `Öffnet ${format(new Date(m.mapVote.opensAt), 'dd.MM.yyyy HH:mm', { locale: de })} Uhr`
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{m.mapVeto.isOpen
|
{m.mapVote.isOpen
|
||||||
? (m.mapVeto.status === 'completed' ? 'Map-Vote abgeschlossen' : 'Map-Vote offen')
|
? (m.mapVote.status === 'completed' ? 'Map-Vote abgeschlossen' : 'Map-Vote offen')
|
||||||
: m.mapVeto.opensAt
|
: m.mapVote.opensAt
|
||||||
? `Map-Vote ab ${format(new Date(m.mapVeto.opensAt), 'HH:mm', { locale: de })} Uhr`
|
? `Map-Vote ab ${format(new Date(m.mapVote.opensAt), 'HH:mm', { locale: de })} Uhr`
|
||||||
: 'Map-Vote bald'}
|
: 'Map-Vote bald'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ type Props = {
|
|||||||
defaultTeamBName?: string | null
|
defaultTeamBName?: string | null
|
||||||
defaultDateISO?: string | null
|
defaultDateISO?: string | null
|
||||||
defaultMap?: string | null
|
defaultMap?: string | null
|
||||||
defaultVetoLeadMinutes?: number
|
defaultVoteLeadMinutes?: number
|
||||||
onSaved?: () => void
|
onSaved?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ export default function EditMatchMetaModal({
|
|||||||
defaultTeamBName,
|
defaultTeamBName,
|
||||||
defaultDateISO,
|
defaultDateISO,
|
||||||
defaultMap,
|
defaultMap,
|
||||||
defaultVetoLeadMinutes = 60,
|
defaultVoteLeadMinutes = 60,
|
||||||
onSaved,
|
onSaved,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
// -------- state
|
// -------- state
|
||||||
@ -48,8 +48,8 @@ export default function EditMatchMetaModal({
|
|||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
})
|
})
|
||||||
const [mapKey, setMapKey] = useState<string>(defaultMap ?? 'lobby_mapveto')
|
const [mapKey, setMapKey] = useState<string>(defaultMap ?? 'lobby_mapvote')
|
||||||
const [vetoLead, setVetoLead] = useState<number>(defaultVetoLeadMinutes)
|
const [voteLead, setVoteLead] = useState<number>(defaultVoteLeadMinutes)
|
||||||
|
|
||||||
const [teams, setTeams] = useState<TeamOption[]>([])
|
const [teams, setTeams] = useState<TeamOption[]>([])
|
||||||
const [loadingTeams, setLoadingTeams] = useState(false)
|
const [loadingTeams, setLoadingTeams] = useState(false)
|
||||||
@ -83,8 +83,8 @@ export default function EditMatchMetaModal({
|
|||||||
setTitle(defaultTitle ?? '')
|
setTitle(defaultTitle ?? '')
|
||||||
setTeamAId(defaultTeamAId ?? '')
|
setTeamAId(defaultTeamAId ?? '')
|
||||||
setTeamBId(defaultTeamBId ?? '')
|
setTeamBId(defaultTeamBId ?? '')
|
||||||
setMapKey(defaultMap ?? 'lobby_mapveto')
|
setMapKey(defaultMap ?? 'lobby_mapvote')
|
||||||
setVetoLead(defaultVetoLeadMinutes)
|
setVoteLead(defaultVoteLeadMinutes)
|
||||||
if (defaultDateISO) {
|
if (defaultDateISO) {
|
||||||
const d = new Date(defaultDateISO)
|
const d = new Date(defaultDateISO)
|
||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
@ -101,7 +101,7 @@ export default function EditMatchMetaModal({
|
|||||||
defaultTeamBId,
|
defaultTeamBId,
|
||||||
defaultDateISO,
|
defaultDateISO,
|
||||||
defaultMap,
|
defaultMap,
|
||||||
defaultVetoLeadMinutes,
|
defaultVoteLeadMinutes,
|
||||||
])
|
])
|
||||||
|
|
||||||
// -------- derived: options
|
// -------- derived: options
|
||||||
@ -143,7 +143,7 @@ export default function EditMatchMetaModal({
|
|||||||
teamBId: teamBId || null,
|
teamBId: teamBId || null,
|
||||||
matchDate: date ? new Date(date).toISOString() : null,
|
matchDate: date ? new Date(date).toISOString() : null,
|
||||||
map: mapKey || null,
|
map: mapKey || null,
|
||||||
vetoLeadMinutes: Number.isFinite(Number(vetoLead)) ? Number(vetoLead) : 60,
|
voteLeadMinutes: Number.isFinite(Number(voteLead)) ? Number(voteLead) : 60,
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`/api/matches/${matchId}/meta`, {
|
const res = await fetch(`/api/matches/${matchId}/meta`, {
|
||||||
@ -253,18 +253,18 @@ export default function EditMatchMetaModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Veto-Lead */}
|
{/* Vote-Lead */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Map-Veto lead (Minuten)</label>
|
<label className="block text-sm font-medium mb-1">Map-Vote lead (Minuten)</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
className="w-full rounded-md border px-3 py-2 bg-white/70 dark:bg-neutral-900/50"
|
className="w-full rounded-md border px-3 py-2 bg-white/70 dark:bg-neutral-900/50"
|
||||||
value={vetoLead}
|
value={voteLead}
|
||||||
onChange={e => setVetoLead(Number(e.target.value))}
|
onChange={e => setVoteLead(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
Zeit vor Matchstart, zu der das Veto öffnet (Standard 60).
|
Zeit vor Matchstart, zu der das Vote öffnet (Standard 60).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
// MapVetoBanner.tsx
|
// MapVoteBanner.tsx
|
||||||
'use client'
|
'use client'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/app/lib/useSSEStore'
|
||||||
import type { MapVetoState } from '../types/mapveto'
|
import type { MapVoteState } from '../types/mapvote'
|
||||||
|
|
||||||
type Props = { match: any; initialNow: number }
|
type Props = { match: any; initialNow: number }
|
||||||
|
|
||||||
export default function MapVetoBanner({ match, initialNow }: Props) {
|
export default function MapVoteBanner({ match, initialNow }: Props) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const { lastEvent } = useSSEStore()
|
const { lastEvent } = useSSEStore()
|
||||||
@ -16,7 +16,7 @@ export default function MapVetoBanner({ match, initialNow }: Props) {
|
|||||||
// ✅ eine Uhr, deterministisch bei Hydration (kommt als Prop vom Server)
|
// ✅ eine Uhr, deterministisch bei Hydration (kommt als Prop vom Server)
|
||||||
const [now, setNow] = useState(initialNow)
|
const [now, setNow] = useState(initialNow)
|
||||||
|
|
||||||
const [state, setState] = useState<MapVetoState | null>(null)
|
const [state, setState] = useState<MapVoteState | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
@ -130,7 +130,7 @@ export default function MapVetoBanner({ match, initialNow }: Props) {
|
|||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
{state?.locked ? (
|
{state?.locked ? (
|
||||||
<span className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200">
|
<span className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200">
|
||||||
Veto abgeschlossen
|
Voting abgeschlossen
|
||||||
</span>
|
</span>
|
||||||
) : isOpen ? (
|
) : isOpen ? (
|
||||||
<span className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200">
|
<span className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200">
|
||||||
@ -1,16 +1,18 @@
|
|||||||
// /app/components/MapVetoPanel.tsx
|
// /app/components/MapVotePanel.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
|
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/app/lib/useSSEStore'
|
||||||
import { MAP_OPTIONS } from '../lib/mapOptions'
|
import MapVoteProfileCard from './MapVoteProfileCard'
|
||||||
import MapVoteProfileCard from './MapVetoProfileCard'
|
|
||||||
import type { Match, MatchPlayer } from '../types/match'
|
import type { Match, MatchPlayer } from '../types/match'
|
||||||
import type { MapVetoState } from '../types/mapveto'
|
import type { MapVoteState } from '../types/mapvote'
|
||||||
|
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import { Player } from '../types/team'
|
import Image from 'next/image'
|
||||||
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
|
|
||||||
type Props = { match: Match }
|
type Props = { match: Match }
|
||||||
|
|
||||||
@ -20,13 +22,15 @@ const getTeamLogo = (logo?: string | null) =>
|
|||||||
const HOLD_MS = 1200
|
const HOLD_MS = 1200
|
||||||
const COMPLETE_THRESHOLD = 1.0
|
const COMPLETE_THRESHOLD = 1.0
|
||||||
|
|
||||||
export default function MapVetoPanel({ match }: Props) {
|
export default function MapVotePanel({ match }: Props) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const { lastEvent } = useSSEStore()
|
const { lastEvent } = useSSEStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const [state, setState] = useState<MapVetoState | null>(null)
|
const [state, setState] = useState<MapVoteState | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [adminEditMode, setAdminEditMode] = useState(false)
|
||||||
|
|
||||||
// --- Zeitpunkt: 1h vor Match-/Demo-Beginn (Fallback) ---
|
// --- Zeitpunkt: 1h vor Match-/Demo-Beginn (Fallback) ---
|
||||||
const opensAtTs = useMemo(() => {
|
const opensAtTs = useMemo(() => {
|
||||||
@ -48,16 +52,20 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
const isLeaderA = !!me?.steamId && match.teamA?.leader?.steamId === me.steamId
|
const isLeaderA = !!me?.steamId && match.teamA?.leader?.steamId === me.steamId
|
||||||
const isLeaderB = !!me?.steamId && match.teamB?.leader?.steamId === me.steamId
|
const isLeaderB = !!me?.steamId && match.teamB?.leader?.steamId === me.steamId
|
||||||
|
|
||||||
|
// Admin-Freeze ableiten
|
||||||
|
const adminEditingBy = state?.adminEdit?.by ?? null
|
||||||
|
const adminEditingEnabled = !!state?.adminEdit?.enabled
|
||||||
|
const isFrozenByAdmin = adminEditingEnabled && adminEditingBy !== me?.steamId
|
||||||
|
|
||||||
const canActForTeamId = useCallback(
|
const canActForTeamId = useCallback(
|
||||||
(teamId?: string | null) => {
|
(teamId?: string | null) => {
|
||||||
if (!teamId) return false
|
if (!teamId) return false
|
||||||
if (isAdmin) return true
|
|
||||||
return (
|
return (
|
||||||
(teamId === match.teamA?.id && isLeaderA) ||
|
(teamId === match.teamA?.id && isLeaderA) ||
|
||||||
(teamId === match.teamB?.id && isLeaderB)
|
(teamId === match.teamB?.id && isLeaderB)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[isAdmin, isLeaderA, isLeaderB, match.teamA?.id, match.teamB?.id],
|
[isLeaderA, isLeaderB, match.teamA?.id, match.teamB?.id],
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Laden / Reload ---
|
// --- Laden / Reload ---
|
||||||
@ -83,19 +91,23 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
}
|
}
|
||||||
}, [match.id])
|
}, [match.id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { load() }, [load])
|
||||||
load()
|
|
||||||
}, [load])
|
|
||||||
|
|
||||||
// --- SSE: live nachladen ---
|
// --- SSE: live nachladen ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastEvent) return
|
if (!lastEvent) return
|
||||||
if (lastEvent.type !== 'map-vote-updated') return
|
if (lastEvent.type !== 'map-vote-updated' && lastEvent.type !== 'map-vote-admin-edit') return
|
||||||
const matchId = lastEvent.payload?.matchId
|
const matchId = lastEvent.payload?.matchId
|
||||||
if (matchId !== match.id) return
|
if (matchId !== match.id) return
|
||||||
load()
|
load()
|
||||||
}, [lastEvent, match.id, load])
|
}, [lastEvent, match.id, load])
|
||||||
|
|
||||||
|
// --- Admin-Edit lokalen Toggle an globalem Zustand spiegeln ---
|
||||||
|
useEffect(() => {
|
||||||
|
const iAmEditing = adminEditingEnabled && adminEditingBy === me?.steamId
|
||||||
|
setAdminEditMode(iAmEditing)
|
||||||
|
}, [adminEditingEnabled, adminEditingBy, me?.steamId])
|
||||||
|
|
||||||
// --- Abgeleitet ---
|
// --- Abgeleitet ---
|
||||||
const opensAt = useMemo(
|
const opensAt = useMemo(
|
||||||
() => (state?.opensAt ? new Date(state.opensAt).getTime() : null),
|
() => (state?.opensAt ? new Date(state.opensAt).getTime() : null),
|
||||||
@ -106,7 +118,11 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
|
|
||||||
const currentStep = state?.steps?.[state?.currentIndex ?? 0]
|
const currentStep = state?.steps?.[state?.currentIndex ?? 0]
|
||||||
const isMyTurn = Boolean(
|
const isMyTurn = Boolean(
|
||||||
isOpen && !state?.locked && currentStep?.teamId && canActForTeamId(currentStep.teamId),
|
isOpen &&
|
||||||
|
!state?.locked &&
|
||||||
|
!isFrozenByAdmin &&
|
||||||
|
currentStep?.teamId &&
|
||||||
|
(canActForTeamId(currentStep.teamId) || (isAdmin && adminEditMode))
|
||||||
)
|
)
|
||||||
|
|
||||||
const mapPool = state?.mapPool ?? []
|
const mapPool = state?.mapPool ?? []
|
||||||
@ -120,7 +136,7 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
return map
|
return map
|
||||||
}, [state?.steps])
|
}, [state?.steps])
|
||||||
|
|
||||||
const fmt = (k: string) => MAP_OPTIONS.find((m) => m.key === k)?.label ?? k
|
const fmt = (k: string) => state?.mapVisuals?.[k]?.label ?? k
|
||||||
|
|
||||||
// --- Aktionen ---
|
// --- Aktionen ---
|
||||||
const handlePickOrBan = async (map: string) => {
|
const handlePickOrBan = async (map: string) => {
|
||||||
@ -137,7 +153,7 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⬅️ Optimistisches Update, bevor SSE kommt:
|
// Optimistisches Update
|
||||||
setState(prev =>
|
setState(prev =>
|
||||||
prev
|
prev
|
||||||
? {
|
? {
|
||||||
@ -148,12 +164,25 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
}
|
}
|
||||||
: prev
|
: prev
|
||||||
)
|
)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
alert('Netzwerkfehler')
|
alert('Netzwerkfehler')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Admin-Edit global toggeln ---
|
||||||
|
async function postAdminEdit(enabled: boolean) {
|
||||||
|
const r = await fetch(`/api/matches/${match.id}/mapvote/admin-edit`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ enabled }),
|
||||||
|
})
|
||||||
|
if (!r.ok) {
|
||||||
|
const j = await r.json().catch(() => ({}))
|
||||||
|
throw new Error(j?.message || 'Konnte Admin-Edit nicht setzen')
|
||||||
|
}
|
||||||
|
return r.json()
|
||||||
|
}
|
||||||
|
|
||||||
// --- Press-and-hold Logik (pro Map) ---
|
// --- Press-and-hold Logik (pro Map) ---
|
||||||
const rafRef = useRef<number | null>(null)
|
const rafRef = useRef<number | null>(null)
|
||||||
const holdStartRef = useRef<number | null>(null)
|
const holdStartRef = useRef<number | null>(null)
|
||||||
@ -249,11 +278,9 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
|
|
||||||
// --- Spielerlisten ableiten (Hooks bleiben IMMER aktiv) ---
|
// --- Spielerlisten ableiten (Hooks bleiben IMMER aktiv) ---
|
||||||
const playersA = useMemo<MatchPlayer[]>(() => {
|
const playersA = useMemo<MatchPlayer[]>(() => {
|
||||||
// 0) Bevorzugt: bereits vorbereitete Team-Spieler am Match selbst
|
|
||||||
const teamPlayers = (match as any)?.teamA?.players as MatchPlayer[] | undefined
|
const teamPlayers = (match as any)?.teamA?.players as MatchPlayer[] | undefined
|
||||||
if (Array.isArray(teamPlayers) && teamPlayers.length) return teamPlayers
|
if (Array.isArray(teamPlayers) && teamPlayers.length) return teamPlayers
|
||||||
|
|
||||||
// 1) Klassischer Weg: match.players via Roster (teamAUsers) filtern
|
|
||||||
const all = (match as any).players as MatchPlayer[] | undefined
|
const all = (match as any).players as MatchPlayer[] | undefined
|
||||||
const teamAUsers = (match as any).teamAUsers as { steamId: string }[] | undefined
|
const teamAUsers = (match as any).teamAUsers as { steamId: string }[] | undefined
|
||||||
if (Array.isArray(all) && Array.isArray(teamAUsers) && teamAUsers.length) {
|
if (Array.isArray(all) && Array.isArray(teamAUsers) && teamAUsers.length) {
|
||||||
@ -261,34 +288,28 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
return all.filter(p => setA.has(p.user.steamId))
|
return all.filter(p => setA.has(p.user.steamId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Fallback: teamId am Player (falls vorhanden)
|
|
||||||
if (Array.isArray(all) && match.teamA?.id) {
|
if (Array.isArray(all) && match.teamA?.id) {
|
||||||
return all.filter(p => (p as any).team?.id === match.teamA?.id)
|
return all.filter(p => (p as any).team?.id === match.teamA?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Letzter Fallback: aus dem Veto-State (kommt aus /mapvote)
|
const votePlayers = state?.teams?.teamA?.players as
|
||||||
const vetoPlayers = state?.teams?.teamA?.players as
|
|
||||||
| Array<{ steamId: string; name?: string | null; avatar?: string | null }>
|
| Array<{ steamId: string; name?: string | null; avatar?: string | null }>
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
if (Array.isArray(vetoPlayers) && vetoPlayers.length) {
|
if (Array.isArray(votePlayers) && votePlayers.length) {
|
||||||
return vetoPlayers.map((p): MatchPlayer => ({
|
return votePlayers.map((p): MatchPlayer => ({
|
||||||
user: {
|
user: {
|
||||||
steamId: p.steamId,
|
steamId: p.steamId,
|
||||||
name: p.name ?? 'Unbekannt',
|
name: p.name ?? 'Unbekannt',
|
||||||
avatar: p.avatar ?? '/assets/img/avatars/default_steam_avatar.jpg',
|
avatar: p.avatar ?? '/assets/img/avatars/default_steam_avatar.jpg',
|
||||||
},
|
},
|
||||||
// wichtig: undefined statt null
|
|
||||||
stats: undefined,
|
stats: undefined,
|
||||||
// falls dein MatchPlayer einen string akzeptiert:
|
|
||||||
// team: (match as any)?.teamA?.name ?? 'Team A',
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}, [match, state?.teams?.teamA?.players])
|
}, [match, state?.teams?.teamA?.players])
|
||||||
|
|
||||||
// ⬇️ ersetzt den bisherigen playersB-Block
|
|
||||||
const playersB = useMemo<MatchPlayer[]>(() => {
|
const playersB = useMemo<MatchPlayer[]>(() => {
|
||||||
const teamPlayers = (match as any)?.teamB?.players as MatchPlayer[] | undefined
|
const teamPlayers = (match as any)?.teamB?.players as MatchPlayer[] | undefined
|
||||||
if (Array.isArray(teamPlayers) && teamPlayers.length) return teamPlayers
|
if (Array.isArray(teamPlayers) && teamPlayers.length) return teamPlayers
|
||||||
@ -304,95 +325,170 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
return all.filter(p => (p as any).team?.id === match.teamB?.id)
|
return all.filter(p => (p as any).team?.id === match.teamB?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const vetoPlayers = state?.teams?.teamB?.players as
|
const votePlayers = state?.teams?.teamB?.players as
|
||||||
| Array<{ steamId: string; name?: string | null; avatar?: string | null }>
|
| Array<{ steamId: string; name?: string | null; avatar?: string | null }>
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
if (Array.isArray(vetoPlayers) && vetoPlayers.length) {
|
if (Array.isArray(votePlayers) && votePlayers.length) {
|
||||||
return vetoPlayers.map((p): MatchPlayer => ({
|
return votePlayers.map((p): MatchPlayer => ({
|
||||||
user: {
|
user: {
|
||||||
steamId: p.steamId,
|
steamId: p.steamId,
|
||||||
name: p.name ?? 'Unbekannt',
|
name: p.name ?? 'Unbekannt',
|
||||||
avatar: p.avatar ?? '/assets/img/avatars/default_steam_avatar.jpg',
|
avatar: p.avatar ?? '/assets/img/avatars/default_steam_avatar.jpg',
|
||||||
},
|
},
|
||||||
stats: undefined,
|
stats: undefined,
|
||||||
// team: (match as any)?.teamB?.name ?? 'Team B',
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}, [match, state?.teams?.teamB?.players])
|
}, [match, state?.teams?.teamB?.players])
|
||||||
|
|
||||||
|
|
||||||
|
const teamAPlayersForRank = useMemo(
|
||||||
|
() => playersA.map(p => ({ premierRank: p.stats?.rankNew ?? 0 })) as any,
|
||||||
|
[playersA]
|
||||||
|
)
|
||||||
|
|
||||||
|
const teamBPlayersForRank = useMemo(
|
||||||
|
() => playersB.map(p => ({ premierRank: p.stats?.rankNew ?? 0 })) as any,
|
||||||
|
[playersB]
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- kleine Helpers ---
|
||||||
|
const editingDisplayName = useMemo(() => {
|
||||||
|
if (!adminEditingBy) return null
|
||||||
|
const all: Array<{ steamId: string; name?: string | null }> = []
|
||||||
|
const pushMaybe = (x: any) => { if (x?.steamId) all.push({ steamId: x.steamId, name: x.name }) }
|
||||||
|
pushMaybe(state?.teams?.teamA?.leader)
|
||||||
|
pushMaybe(state?.teams?.teamB?.leader)
|
||||||
|
;(state?.teams?.teamA?.players ?? []).forEach(pushMaybe)
|
||||||
|
;(state?.teams?.teamB?.players ?? []).forEach(pushMaybe)
|
||||||
|
return all.find(p => p.steamId === adminEditingBy)?.name || 'Admin'
|
||||||
|
}, [adminEditingBy, state?.teams])
|
||||||
|
|
||||||
const showLoading = isLoading && !state
|
const showLoading = isLoading && !state
|
||||||
const showError = !!error && !state
|
const showError = !!error && !state
|
||||||
|
|
||||||
|
const sortedMapPool = useMemo(() => {
|
||||||
|
// nach Anzeige-Label sortieren (fallback: key), case-insensitive, deutsch
|
||||||
|
return [...(state?.mapPool ?? [])].sort((a, b) =>
|
||||||
|
(state?.mapVisuals?.[a]?.label ?? a)
|
||||||
|
.localeCompare(state?.mapVisuals?.[b]?.label ?? b, 'de', { sensitivity: 'base' })
|
||||||
|
)
|
||||||
|
}, [state?.mapPool, state?.mapVisuals])
|
||||||
|
|
||||||
|
// --- UI ---
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{showLoading ? (
|
{showLoading ? (
|
||||||
<div className="p-4">Lade Map-Voting…</div>
|
<div className="p-4">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
) : showError ? (
|
) : showError ? (
|
||||||
<div className="p-4 text-red-600">{error}</div>
|
<div className="p-4 text-red-600">{error}</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="grid grid-cols-3 items-center mb-3">
|
||||||
<h3 className="text-lg font-semibold">Map-Vote</h3>
|
{/* Linke Spalte */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
color="gray"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => router.push(`/match-details/${match.id}`)}
|
||||||
|
>
|
||||||
|
← Zurück
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mittlere Spalte (zentriert) */}
|
||||||
|
<h3 className="text-lg font-semibold text-center">Voting</h3>
|
||||||
|
|
||||||
|
{/* Rechte Spalte */}
|
||||||
|
<div className="flex justify-end items-center gap-2">
|
||||||
<div className="text-sm opacity-80">
|
<div className="text-sm opacity-80">
|
||||||
Modus: BO{match.bestOf ?? state?.bestOf ?? 3}
|
Modus: BO{match.bestOf ?? state?.bestOf ?? 3}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Button
|
<>
|
||||||
color="red"
|
<Button
|
||||||
variant="outline"
|
color={adminEditMode ? 'teal' : 'gray'}
|
||||||
size="sm"
|
variant={adminEditMode ? 'solid' : 'outline'}
|
||||||
className="ml-3"
|
size="sm"
|
||||||
title="Map-Vote zurücksetzen"
|
className="ml-2"
|
||||||
onClick={async () => {
|
title={adminEditMode ? 'Admin-Bearbeitung beenden' : 'Map-Vote als Admin bearbeiten'}
|
||||||
if (!confirm('Map-Vote wirklich zurücksetzen? Alle bisherigen Picks/Bans gehen verloren.')) return
|
onClick={async () => {
|
||||||
try {
|
// Optimistisch lokal toggeln
|
||||||
const r = await fetch(`/api/matches/${match.id}/mapvote/reset`, { method: 'POST' })
|
const next = !adminEditMode
|
||||||
if (!r.ok) {
|
setAdminEditMode(next)
|
||||||
const j = await r.json().catch(() => ({}))
|
try {
|
||||||
alert(j.message ?? 'Reset fehlgeschlagen')
|
await postAdminEdit(next) // globaler Freeze on/off
|
||||||
return
|
await load()
|
||||||
|
} catch (e: any) {
|
||||||
|
setAdminEditMode(v => !v) // rollback
|
||||||
|
alert(e?.message ?? 'Fehler beim Umschalten des Admin-Edits')
|
||||||
}
|
}
|
||||||
// SSE feuert ohnehin; zusätzlich lokal nachladen:
|
}}
|
||||||
await load()
|
>
|
||||||
} catch {
|
{adminEditMode ? 'Bearbeiten: AN' : 'Bearbeiten'}
|
||||||
alert('Netzwerkfehler beim Reset')
|
</Button>
|
||||||
}
|
|
||||||
}}
|
<Button
|
||||||
/>
|
color="red"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="ml-2"
|
||||||
|
title="Map-Vote zurücksetzen"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!confirm('Map-Vote wirklich zurücksetzen? Alle bisherigen Picks/Bans gehen verloren.')) return
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/matches/${match.id}/mapvote/reset`, { method: 'POST' })
|
||||||
|
if (!r.ok) {
|
||||||
|
const j = await r.json().catch(() => ({}))
|
||||||
|
alert(j.message ?? 'Reset fehlgeschlagen')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await load()
|
||||||
|
} catch {
|
||||||
|
alert('Netzwerkfehler beim Reset')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Countdown / Status */}
|
{/* Countdown / Status */}
|
||||||
{!isOpen && (
|
<div className="mb-4 flex flex-col items-center gap-2">
|
||||||
<div className="mb-4 text-sm">
|
|
||||||
<span className="inline-block px-2 py-1 rounded bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100">
|
|
||||||
Öffnet in {formatCountdown(msToOpen)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Countdown / Status ganz oben und größer */}
|
|
||||||
<div className="mb-4 flex justify-center">
|
|
||||||
{state?.locked ? (
|
{state?.locked ? (
|
||||||
<span className="block text-lg font-semibold px-3 py-2 rounded bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 text-center">
|
<span className="block text-lg font-semibold px-3 py-2 rounded bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 text-center">
|
||||||
✅ Veto abgeschlossen
|
✅ Voting abgeschlossen
|
||||||
</span>
|
</span>
|
||||||
) : isOpen ? (
|
) : isOpen ? (
|
||||||
isMyTurn ? (
|
isFrozenByAdmin ? (
|
||||||
|
<span className="block text-lg font-semibold px-3 py-2 rounded bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100 text-center">
|
||||||
|
🔒 Admin-Edit aktiv – Voting pausiert
|
||||||
|
{editingDisplayName ? ` (von ${editingDisplayName})` : ''}
|
||||||
|
</span>
|
||||||
|
) : isMyTurn ? (
|
||||||
<span className="block text-lg font-semibold px-3 py-2 rounded bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200 text-center">
|
<span className="block text-lg font-semibold px-3 py-2 rounded bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200 text-center">
|
||||||
✋ Halte gedrückt, um zu bestätigen
|
{currentStep?.action === 'ban'
|
||||||
|
? '🚫 Dein Team darf bannen'
|
||||||
|
: currentStep?.action === 'pick'
|
||||||
|
? '✅ Dein Team darf picken'
|
||||||
|
: 'Du bist dran'}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="block text-lg font-semibold px-3 py-2 rounded bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200 text-center">
|
<span className="block text-lg font-semibold px-3 py-2 rounded bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200 text-center">
|
||||||
⏳ Wartet auf
|
⏳ Wartet auf
|
||||||
{currentStep?.teamId === match.teamA?.id ? match.teamA.name : match.teamB.name}
|
{currentStep?.teamId === match.teamA?.id
|
||||||
(Leader/Admin)
|
? match.teamA.name
|
||||||
|
: match.teamB.name}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@ -402,7 +498,7 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<span className="block mt-2 text-base font-medium px-3 py-2 rounded bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 text-center">
|
<span className="block text-base font-medium px-3 py-2 rounded bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 text-center">
|
||||||
{error}
|
{error}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -410,9 +506,25 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
|
|
||||||
{/* Hauptbereich */}
|
{/* Hauptbereich */}
|
||||||
{state && (
|
{state && (
|
||||||
<div className="mt-2 flex items-start gap-4 justify-between">
|
<div className="mt-0 grid grid-cols-[0.8fr_1.4fr_0.8fr] gap-10 items-start">
|
||||||
{/* Links – Team A */}
|
{/* Linke Spalte – Team A */}
|
||||||
<aside className="hidden lg:flex lg:flex-col gap-2 w-56">
|
<div className="flex flex-col items-start gap-3">
|
||||||
|
{/* Teamkopf A */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={getTeamLogo(match.teamA?.logo)}
|
||||||
|
alt={match.teamA?.name ?? 'Team A'}
|
||||||
|
className="w-12 h-12 rounded-full border bg-white dark:bg-neutral-900 object-contain"
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="font-bold text-lg truncate">{match.teamA?.name ?? 'Team A'}</div>
|
||||||
|
</div>
|
||||||
|
<TeamPremierRankBadge players={teamAPlayersForRank} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spieler A */}
|
||||||
{playersA.map((p: MatchPlayer) => (
|
{playersA.map((p: MatchPlayer) => (
|
||||||
<MapVoteProfileCard
|
<MapVoteProfileCard
|
||||||
key={p.user.steamId}
|
key={p.user.steamId}
|
||||||
@ -421,6 +533,7 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
avatar={p.user.avatar}
|
avatar={p.user.avatar}
|
||||||
rank={p.stats?.rankNew ?? 0}
|
rank={p.stats?.rankNew ?? 0}
|
||||||
matchType={match.matchType}
|
matchType={match.matchType}
|
||||||
|
onClick={() => router.push(`/profile/${p.user.steamId}`)}
|
||||||
isLeader={
|
isLeader={
|
||||||
(state?.teams?.teamA?.leader?.steamId ?? match.teamA?.leader?.steamId) ===
|
(state?.teams?.teamA?.leader?.steamId ?? match.teamA?.leader?.steamId) ===
|
||||||
p.user.steamId
|
p.user.steamId
|
||||||
@ -432,12 +545,12 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</aside>
|
</div>
|
||||||
|
|
||||||
{/* Mitte – Maps (Hold-to-confirm) */}
|
{/* Mitte – Mappool */}
|
||||||
<main className="max-w-sm flex-shrink-0">
|
<main className="w-full flex-1 max-w-xl">
|
||||||
<ul className="flex flex-col gap-1.5">
|
<ul className="flex flex-col gap-3">
|
||||||
{mapPool.map((map) => {
|
{sortedMapPool.map((map) => {
|
||||||
const decision = decisionByMap.get(map)
|
const decision = decisionByMap.get(map)
|
||||||
const status = decision?.action ?? null // 'ban' | 'pick' | 'decider' | null
|
const status = decision?.action ?? null // 'ban' | 'pick' | 'decider' | null
|
||||||
const teamId = decision?.teamId ?? null
|
const teamId = decision?.teamId ?? null
|
||||||
@ -449,28 +562,13 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
const intent = isAvailable ? currentStep?.action : null
|
const intent = isAvailable ? currentStep?.action : null
|
||||||
const intentStyles =
|
const intentStyles =
|
||||||
intent === 'ban'
|
intent === 'ban'
|
||||||
? {
|
? { hover: 'hover:bg-red-50 dark:hover:bg-red-950', progress: 'bg-red-200/60 dark:bg-red-800/40' }
|
||||||
ring: '',
|
|
||||||
border: '',
|
|
||||||
hover: 'hover:bg-red-50 dark:hover:bg-red-950',
|
|
||||||
progress: 'bg-red-200/60 dark:bg-red-800/40',
|
|
||||||
}
|
|
||||||
: intent === 'pick'
|
: intent === 'pick'
|
||||||
? {
|
? { hover: 'hover:bg-green-50 dark:hover:bg-green-950', progress: 'bg-green-200/60 dark:bg-green-800/40' }
|
||||||
ring: '',
|
: { hover: 'hover:bg-blue-50 dark:hover:bg-blue-950', progress: 'bg-blue-200/60 dark:bg-blue-800/40' }
|
||||||
border: '',
|
|
||||||
hover: 'hover:bg-green-50 dark:hover:bg-green-950',
|
|
||||||
progress: 'bg-green-200/60 dark:bg-green-800/40',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
ring: '',
|
|
||||||
border: '',
|
|
||||||
hover: 'hover:bg-blue-50 dark:hover:bg-blue-950',
|
|
||||||
progress: 'bg-blue-200/60 dark:bg-blue-800/40',
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
'relative flex items-center justify-between gap-2 rounded-md border p-2.5 transition select-none'
|
'relative flex items-center justify-between gap-2 rounded-md border border-neutral-500 p-2.5 transition select-none'
|
||||||
|
|
||||||
const visualTaken =
|
const visualTaken =
|
||||||
status === 'ban'
|
status === 'ban'
|
||||||
@ -479,8 +577,8 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
? 'bg-blue-50/60 dark:bg-blue-900/20 border-blue-200 dark:border-blue-900/40'
|
? 'bg-blue-50/60 dark:bg-blue-900/20 border-blue-200 dark:border-blue-900/40'
|
||||||
: 'bg-neutral-100 dark:bg-neutral-800 border-neutral-300 dark:border-neutral-700'
|
: 'bg-neutral-100 dark:bg-neutral-800 border-neutral-300 dark:border-neutral-700'
|
||||||
|
|
||||||
const visualAvailable = `bg-white dark:bg-neutral-900 ${intentStyles.border} ring-1 ${intentStyles.ring} ${intentStyles.hover} cursor-pointer`
|
const visualAvailable = `bg-white dark:bg-neutral-900 ${intentStyles.hover} cursor-pointer`
|
||||||
const visualDisabled = 'bg-white dark:bg-neutral-900 border-neutral-300 dark:border-neutral-700'
|
const visualDisabled = `bg-white dark:bg-neutral-900 border-neutral-300 dark:border-neutral-700 cursor-not-allowed ${isFrozenByAdmin ? 'opacity-60' : ''}`
|
||||||
const visualClasses = taken ? visualTaken : isAvailable ? visualAvailable : visualDisabled
|
const visualClasses = taken ? visualTaken : isAvailable ? visualAvailable : visualDisabled
|
||||||
|
|
||||||
// Decider-Team bestimmen (falls nötig)
|
// Decider-Team bestimmen (falls nötig)
|
||||||
@ -497,26 +595,44 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
const progress = progressByMap[map] ?? 0
|
const progress = progressByMap[map] ?? 0
|
||||||
const showProgress = isAvailable && progress > 0 && progress < 1
|
const showProgress = isAvailable && progress > 0 && progress < 1
|
||||||
|
|
||||||
|
const bg = state?.mapVisuals?.[map]?.bg ?? `/assets/img/maps/${map}/1.jpg`
|
||||||
|
|
||||||
|
const disabledTitle = isFrozenByAdmin
|
||||||
|
? 'Ein Admin bearbeitet gerade – Voting gesperrt'
|
||||||
|
: 'Nur der Team-Leader (oder Admin) darf wählen'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={map}
|
key={map}
|
||||||
className="grid grid-cols-[24px_1fr_24px] items-center gap-2"
|
className="grid grid-cols-[max-content_1fr_max-content] items-center gap-2"
|
||||||
>
|
>
|
||||||
{/* linker Slot */}
|
{/* linker Slot */}
|
||||||
{pickedByA ? (
|
{pickedByA ? (
|
||||||
<img
|
<img
|
||||||
src={getTeamLogo(match.teamA?.logo)}
|
src={getTeamLogo(match.teamA?.logo)}
|
||||||
alt={match.teamA?.name ?? 'Team A'}
|
alt={match.teamA?.name ?? 'Team A'}
|
||||||
className="w-6 h-6 rounded-full border bg-white dark:bg-neutral-900"
|
className={[
|
||||||
|
"w-10 h-10 rounded-full border bg-white dark:bg-neutral-900 object-contain",
|
||||||
|
"transition-opacity transition-transform duration-300 ease-out transform-gpu will-change-transform",
|
||||||
|
pickedByA ? "opacity-100 scale-100" : "opacity-0 scale-90 pointer-events-none"
|
||||||
|
].join(" ")}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-6 h-6" />
|
<div className="w-10 h-10" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Button */}
|
{/* Button */}
|
||||||
<Button
|
<Button
|
||||||
className={`${baseClasses} ${visualClasses} w-full text-left relative`}
|
variant="link"
|
||||||
|
color="transparent"
|
||||||
|
className={[
|
||||||
|
baseClasses,
|
||||||
|
visualClasses,
|
||||||
|
"w-full text-left relative overflow-hidden group",
|
||||||
|
"transition-colors duration-300 ease-in-out"
|
||||||
|
].join(" ")}
|
||||||
disabled={!isAvailable}
|
disabled={!isAvailable}
|
||||||
|
size="full"
|
||||||
title={
|
title={
|
||||||
taken
|
taken
|
||||||
? status === 'ban'
|
? status === 'ban'
|
||||||
@ -526,7 +642,7 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
: 'Decider'
|
: 'Decider'
|
||||||
: isAvailable
|
: isAvailable
|
||||||
? 'Zum Bestätigen gedrückt halten'
|
? 'Zum Bestätigen gedrückt halten'
|
||||||
: 'Nur der Team-Leader (oder Admin) darf wählen'
|
: disabledTitle
|
||||||
}
|
}
|
||||||
onMouseDown={() => onHoldStart(map, isAvailable)}
|
onMouseDown={() => onHoldStart(map, isAvailable)}
|
||||||
onMouseUp={() => cancelOrSubmitIfComplete(map)}
|
onMouseUp={() => cancelOrSubmitIfComplete(map)}
|
||||||
@ -535,27 +651,104 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
onTouchEnd={onTouchEnd(map)}
|
onTouchEnd={onTouchEnd(map)}
|
||||||
onTouchCancel={onTouchEnd(map)}
|
onTouchCancel={onTouchEnd(map)}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-center bg-auto filter opacity-30 transition-opacity duration-300"
|
||||||
|
style={{ backgroundImage: `url('${bg}')` }}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Fortschrittsbalken */}
|
{/* Fortschrittsbalken */}
|
||||||
{showProgress && (
|
{showProgress && (
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={`absolute inset-y-0 left-0 rounded-md ${intentStyles.progress} pointer-events-none z-0`}
|
className={`absolute inset-y-0 left-0 rounded-md ${intentStyles.progress} pointer-events-none z-10`}
|
||||||
style={{ width: `${Math.round(progress * 100)}%` }}
|
style={{ width: `${Math.round(progress * 100)}%` }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Inhalt */}
|
{/* Fixe Ban/Pick-Pills bei bereits entschiedenen Maps (inkl. Decider = Pick) */}
|
||||||
<div className="flex-1 min-w-0 relative z-[1] flex flex-col items-center justify-center text-center">
|
{taken && (status === 'ban' || status === 'pick' || status === 'decider') && (
|
||||||
<span className="text-[13px] font-medium truncate">{fmt(map)}</span>
|
<>
|
||||||
|
{/* linke Seite (Team A) */}
|
||||||
|
{(
|
||||||
|
(status === 'ban' && teamId === match.teamA?.id) ||
|
||||||
|
(status === 'pick' && effectiveTeamId === match.teamA?.id) ||
|
||||||
|
(status === 'decider' && effectiveTeamId === match.teamA?.id)
|
||||||
|
) && (
|
||||||
|
<span
|
||||||
|
className={`pointer-events-none absolute left-2 top-1/2 -translate-y-1/2
|
||||||
|
px-2 py-0.5 text-[11px] font-semibold rounded transition duration-300 ease-out
|
||||||
|
${status === 'ban' ? 'bg-red-600 text-white' : 'bg-green-600 text-white'}`}
|
||||||
|
style={{ zIndex: 25 }}
|
||||||
|
>
|
||||||
|
{status === 'ban' ? 'Ban' : 'Pick'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* rechte Seite (Team B) */}
|
||||||
|
{(
|
||||||
|
(status === 'ban' && teamId === match.teamB?.id) ||
|
||||||
|
(status === 'pick' && effectiveTeamId === match.teamB?.id) ||
|
||||||
|
(status === 'decider' && effectiveTeamId === match.teamB?.id)
|
||||||
|
) && (
|
||||||
|
<span
|
||||||
|
className={`pointer-events-none absolute right-2 top-1/2 -translate-y-1/2
|
||||||
|
px-2 py-0.5 text-[11px] font-semibold rounded
|
||||||
|
${status === 'ban' ? 'bg-red-600 text-white' : 'bg-green-600 text-white'}`}
|
||||||
|
style={{ zIndex: 25 }}
|
||||||
|
>
|
||||||
|
{status === 'ban' ? 'Ban' : 'Pick'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Hover-Ban/Pick-Pills */}
|
||||||
|
{isAvailable && (intent === 'ban' || intent === 'pick') && (
|
||||||
|
<>
|
||||||
|
{/* Team A (links) */}
|
||||||
|
{currentStep?.teamId === match.teamA?.id && (
|
||||||
|
<span
|
||||||
|
className={`pointer-events-none absolute left-2 top-1/2 -translate-y-1/2
|
||||||
|
px-2 py-0.5 text-[11px] font-semibold rounded
|
||||||
|
opacity-0 group-hover:opacity-100 transition
|
||||||
|
${intent === 'ban'
|
||||||
|
? 'bg-red-600 text-white'
|
||||||
|
: 'bg-green-600 text-white'}`}
|
||||||
|
style={{ zIndex: 25 }}
|
||||||
|
>
|
||||||
|
{intent === 'ban' ? 'Ban' : 'Pick'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Team B (rechts) */}
|
||||||
|
{currentStep?.teamId === match.teamB?.id && (
|
||||||
|
<span
|
||||||
|
className={`pointer-events-none absolute right-2 top-1/2 -translate-y-1/2
|
||||||
|
px-2 py-0.5 text-[11px] font-semibold rounded
|
||||||
|
opacity-0 group-hover:opacity-100 transition
|
||||||
|
${intent === 'ban'
|
||||||
|
? 'bg-red-600 text-white'
|
||||||
|
: 'bg-green-600 text-white'}`}
|
||||||
|
style={{ zIndex: 25 }}
|
||||||
|
>
|
||||||
|
{intent === 'ban' ? 'Ban' : 'Pick'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Button-Inhalt */}
|
||||||
|
<div className="flex-1 min-w-0 relative z-20 flex flex-col items-center justify-center text-center">
|
||||||
|
<span className="text-[13px] font-medium truncate text-white font-semibold uppercase">{fmt(map)}</span>
|
||||||
|
|
||||||
{status === 'ban' && (
|
{status === 'ban' && (
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="absolute inset-0 pointer-events-none flex items-center justify-center z-[2]"
|
className="absolute inset-0 pointer-events-none flex items-center justify-center z-30"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
className="w-8 h-8 opacity-30 text-red-600"
|
className="w-12 h-12 sm:w-14 sm:h-14 opacity-30 text-red-600"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
>
|
>
|
||||||
<path d="M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 1 0 5.7 7.11L10.59 12l-4.9 4.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.9a1 1 0 0 0 1.41-1.41L13.41 12l4.9-4.89a1 1 0 0 0-.01-1.4Z" />
|
<path d="M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 1 0 5.7 7.11L10.59 12l-4.9 4.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.9a1 1 0 0 0 1.41-1.41L13.41 12l4.9-4.89a1 1 0 0 0-.01-1.4Z" />
|
||||||
@ -570,10 +763,14 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
<img
|
<img
|
||||||
src={getTeamLogo(match.teamB?.logo)}
|
src={getTeamLogo(match.teamB?.logo)}
|
||||||
alt={match.teamB?.name ?? 'Team B'}
|
alt={match.teamB?.name ?? 'Team B'}
|
||||||
className="w-6 h-6 rounded-full border bg-white dark:bg-neutral-900"
|
className={[
|
||||||
|
"w-10 h-10 rounded-full border bg-white dark:bg-neutral-900 object-contain",
|
||||||
|
"transition-opacity transition-transform duration-300 ease-out transform-gpu will-change-transform",
|
||||||
|
pickedByB ? "opacity-100 scale-100" : "opacity-0 scale-90 pointer-events-none"
|
||||||
|
].join(" ")}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-6 h-6" />
|
<div className="w-10 h-10" />
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
@ -581,8 +778,24 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Rechts – Team B */}
|
{/* Rechte Spalte – Team B */}
|
||||||
<aside className="hidden lg:flex lg:flex-col gap-2 w-56">
|
<div className="flex flex-col items-end gap-3">
|
||||||
|
{/* Teamkopf B */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<TeamPremierRankBadge players={teamBPlayersForRank} />
|
||||||
|
<div className="min-w-0 text-right">
|
||||||
|
<div className="font-bold text-lg truncate">{match.teamB?.name ?? 'Team B'}</div>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={getTeamLogo(match.teamB?.logo)}
|
||||||
|
alt={match.teamB?.name ?? 'Team B'}
|
||||||
|
className="w-12 h-12 rounded-full border bg-white dark:bg-neutral-900 object-contain"
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spieler B */}
|
||||||
{playersB.map((p: MatchPlayer) => (
|
{playersB.map((p: MatchPlayer) => (
|
||||||
<MapVoteProfileCard
|
<MapVoteProfileCard
|
||||||
key={p.user.steamId}
|
key={p.user.steamId}
|
||||||
@ -591,6 +804,7 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
avatar={p.user.avatar}
|
avatar={p.user.avatar}
|
||||||
rank={p.stats?.rankNew ?? 0}
|
rank={p.stats?.rankNew ?? 0}
|
||||||
matchType={match.matchType}
|
matchType={match.matchType}
|
||||||
|
onClick={() => router.push(`/profile/${p.user.steamId}`)}
|
||||||
isLeader={
|
isLeader={
|
||||||
(state?.teams?.teamB?.leader?.steamId ?? match.teamB?.leader?.steamId) ===
|
(state?.teams?.teamB?.leader?.steamId ?? match.teamB?.leader?.steamId) ===
|
||||||
p.user.steamId
|
p.user.steamId
|
||||||
@ -602,7 +816,7 @@ export default function MapVetoPanel({ match }: Props) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</aside>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -5,22 +5,21 @@ import PremierRankBadge from './PremierRankBadge'
|
|||||||
type Side = 'A' | 'B'
|
type Side = 'A' | 'B'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
side: Side // 'A' = linke Spalte, 'B' = rechte Spalte
|
side: Side
|
||||||
name: string
|
name: string
|
||||||
avatar?: string | null
|
avatar?: string | null
|
||||||
rank?: number // Zahl aus deinen Stats
|
rank?: number
|
||||||
matchType?: 'premier' | 'competitive' | string
|
matchType?: 'premier' | 'competitive' | string
|
||||||
isLeader?: boolean
|
isLeader?: boolean
|
||||||
isActiveTurn?: boolean // pulsiert, wenn dieses Team am Zug ist
|
isActiveTurn?: boolean
|
||||||
onClick?: () => void // optional
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MapVetoProfileCard({
|
export default function MapVoteProfileCard({
|
||||||
side,
|
side,
|
||||||
name,
|
name,
|
||||||
avatar,
|
avatar,
|
||||||
rank = 0,
|
rank = 0,
|
||||||
matchType = 'premier',
|
|
||||||
isLeader = false,
|
isLeader = false,
|
||||||
isActiveTurn = false,
|
isActiveTurn = false,
|
||||||
onClick,
|
onClick,
|
||||||
@ -32,20 +31,24 @@ export default function MapVetoProfileCard({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={[
|
className={[
|
||||||
'group relative w-full',
|
'group relative w-full rounded-xl shadow-md',
|
||||||
isRight ? 'ml-auto text-right' : 'mr-auto text-left',
|
isRight ? 'ml-auto text-right' : 'mr-auto text-left',
|
||||||
].join(' ')}
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
title={isLeader ? `${name} (Leader)` : name}
|
title={isLeader ? `${name} (Leader)` : name}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
'flex items-center gap-3 rounded-xl border bg-white/90 dark:bg-neutral-800/90',
|
'relative flex items-center gap-3 rounded-xl border dark:border-neutral-700 shadow-sm px-3 py-2',
|
||||||
'dark:border-neutral-700 shadow-sm px-3 py-2 transition',
|
'transition-colors duration-300 ease-in-out justify-between',
|
||||||
isActiveTurn
|
isActiveTurn
|
||||||
? 'ring-2 ring-blue-500/30 shadow-md'
|
? 'bg-emerald-50 dark:bg-emerald-900/20 hover:bg-emerald-100 dark:hover:bg-emerald-800/30'
|
||||||
: 'ring-1 ring-black/5 hover:ring-black/10',
|
: 'bg-white/90 dark:bg-neutral-800/90 hover:bg-neutral-200/10',
|
||||||
isRight ? 'flex-row-reverse' : 'flex-row',
|
isRight ? 'flex-row-reverse' : 'flex-row',
|
||||||
].join(' ')}
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
>
|
>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div className="relative shrink-0">
|
<div className="relative shrink-0">
|
||||||
@ -64,24 +67,29 @@ export default function MapVetoProfileCard({
|
|||||||
].join(' ')}
|
].join(' ')}
|
||||||
title="Team-Leader"
|
title="Team-Leader"
|
||||||
>
|
>
|
||||||
{/* Stern-Icon */}
|
<svg
|
||||||
<svg viewBox="0 0 24 24" className="w-3.5 h-3.5" fill="currentColor" aria-hidden>
|
viewBox="0 0 24 24"
|
||||||
<path d="m12 17.27 6.18 3.73-1.64-7.03L21 9.24l-7.19-.62L12 2 10.19 8.62 3 9.24l4.46 4.73L5.82 21z"/>
|
className="w-3.5 h-3.5"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
<path d="m12 17.27 6.18 3.73-1.64-7.03L21 9.24l-7.19-.62L12 2 10.19 8.62 3 9.24l4.46 4.73L5.82 21z" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text + Rank */}
|
{/* Name + Status */}
|
||||||
<div className={['min-w-0', isRight ? 'items-end text-right' : 'items-start text-left', 'flex flex-col'].join(' ')}>
|
<div
|
||||||
<div className="flex items-center gap-2 max-w-[160px]">
|
className={[
|
||||||
<span className="truncate font-medium text-gray-900 dark:text-neutral-100">
|
'min-w-0 flex-1',
|
||||||
{name}
|
isRight ? 'items-end text-right' : 'items-start text-left',
|
||||||
</span>
|
'flex flex-col',
|
||||||
<span className="opacity-90">
|
].join(' ')}
|
||||||
<PremierRankBadge rank={rank ?? 0} />
|
>
|
||||||
</span>
|
<span className="truncate font-medium text-gray-900 dark:text-neutral-100">
|
||||||
</div>
|
{name}
|
||||||
|
</span>
|
||||||
{isActiveTurn ? (
|
{isActiveTurn ? (
|
||||||
<span className="mt-0.5 text-[11px] font-medium text-blue-700 dark:text-blue-300">
|
<span className="mt-0.5 text-[11px] font-medium text-blue-700 dark:text-blue-300">
|
||||||
am Zug …
|
am Zug …
|
||||||
@ -92,6 +100,9 @@ export default function MapVetoProfileCard({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* PremierRank ganz außen */}
|
||||||
|
<PremierRankBadge rank={rank ?? 0} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@ -22,7 +22,7 @@ import type { EditSide } from './EditMatchPlayersModal'
|
|||||||
import type { Match, MatchPlayer } from '../types/match'
|
import type { Match, MatchPlayer } from '../types/match'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import { MAP_OPTIONS } from '../lib/mapOptions'
|
import { MAP_OPTIONS } from '../lib/mapOptions'
|
||||||
import MapVetoBanner from './MapVetoBanner'
|
import MapVoteBanner from './MapVoteBanner'
|
||||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
import { useSSEStore } from '@/app/lib/useSSEStore'
|
||||||
import { Team } from '../types/team'
|
import { Team } from '../types/team'
|
||||||
import Alert from './Alert'
|
import Alert from './Alert'
|
||||||
@ -71,7 +71,7 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
const mapKey = normalizeMapKey(match.map)
|
const mapKey = normalizeMapKey(match.map)
|
||||||
const mapLabel =
|
const mapLabel =
|
||||||
MAP_OPTIONS.find(opt => opt.key === mapKey)?.label ??
|
MAP_OPTIONS.find(opt => opt.key === mapKey)?.label ??
|
||||||
MAP_OPTIONS.find(opt => opt.key === 'lobby_mapveto')?.label ??
|
MAP_OPTIONS.find(opt => opt.key === 'lobby_mapvote')?.label ??
|
||||||
'Unbekannte Map'
|
'Unbekannte Map'
|
||||||
|
|
||||||
/* ─── Match-Zeitpunkt ─────────────────────────────────────── */
|
/* ─── Match-Zeitpunkt ─────────────────────────────────────── */
|
||||||
@ -81,24 +81,24 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
/* ─── Modal-State: null = geschlossen, 'A' / 'B' = offen ─── */
|
/* ─── Modal-State: null = geschlossen, 'A' / 'B' = offen ─── */
|
||||||
const [editSide, setEditSide] = useState<EditSide | null>(null)
|
const [editSide, setEditSide] = useState<EditSide | null>(null)
|
||||||
|
|
||||||
/* ─── Live-Uhr (für Veto-Zeitpunkt) ───────────────────────── */
|
/* ─── Live-Uhr (für vote-Zeitpunkt) ───────────────────────── */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = setInterval(() => setNow(Date.now()), 1000)
|
const id = setInterval(() => setNow(Date.now()), 1000)
|
||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const vetoOpensAtTs = useMemo(() => {
|
const voteOpensAtTs = useMemo(() => {
|
||||||
const base = match.mapVeto?.opensAt
|
const base = match.mapVote?.opensAt
|
||||||
? new Date(match.mapVeto.opensAt).getTime()
|
? new Date(match.mapVote.opensAt).getTime()
|
||||||
: new Date(match.matchDate ?? match.demoDate ?? initialNow).getTime() - 60 * 60 * 1000
|
: new Date(match.matchDate ?? match.demoDate ?? initialNow).getTime() - 60 * 60 * 1000
|
||||||
return base
|
return base
|
||||||
}, [match.mapVeto?.opensAt, match.matchDate, match.demoDate, initialNow])
|
}, [match.mapVote?.opensAt, match.matchDate, match.demoDate, initialNow])
|
||||||
|
|
||||||
const endDate = new Date(vetoOpensAtTs)
|
const endDate = new Date(voteOpensAtTs)
|
||||||
const mapVetoStarted = (match.mapVeto?.isOpen ?? false) || now >= vetoOpensAtTs
|
const mapvoteStarted = (match.mapVote?.isOpen ?? false) || now >= voteOpensAtTs
|
||||||
|
|
||||||
const showEditA = canEditA && !mapVetoStarted
|
const showEditA = canEditA && !mapvoteStarted
|
||||||
const showEditB = canEditB && !mapVetoStarted
|
const showEditB = canEditB && !mapvoteStarted
|
||||||
|
|
||||||
/* ─── SSE-Listener ─────────────────────────────────────────── */
|
/* ─── SSE-Listener ─────────────────────────────────────────── */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -271,7 +271,7 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
<strong>Score:</strong> {match.scoreA ?? 0}:{match.scoreB ?? 0}
|
<strong>Score:</strong> {match.scoreA ?? 0}:{match.scoreB ?? 0}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MapVetoBanner match={match} initialNow={initialNow} />
|
<MapVoteBanner match={match} initialNow={initialNow} />
|
||||||
|
|
||||||
{/* ───────── Team-Blöcke ───────── */}
|
{/* ───────── Team-Blöcke ───────── */}
|
||||||
<div className="border-t pt-4 mt-4 space-y-10">
|
<div className="border-t pt-4 mt-4 space-y-10">
|
||||||
@ -369,7 +369,7 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
defaultTeamBName={match.teamB?.name ?? null}
|
defaultTeamBName={match.teamB?.name ?? null}
|
||||||
defaultDateISO={match.matchDate ?? match.demoDate ?? null}
|
defaultDateISO={match.matchDate ?? match.demoDate ?? null}
|
||||||
defaultMap={match.map ?? null}
|
defaultMap={match.map ?? null}
|
||||||
defaultVetoLeadMinutes={60}
|
defaultVoteLeadMinutes={60}
|
||||||
onSaved={() => { router.refresh() }}
|
onSaved={() => { router.refresh() }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export default function TeamCard({
|
|||||||
const [joining, setJoining] = useState(false)
|
const [joining, setJoining] = useState(false)
|
||||||
|
|
||||||
const isRequested = Boolean(invitationId)
|
const isRequested = Boolean(invitationId)
|
||||||
const isDisabled = joining || currentUserSteamId === team.leader
|
const isDisabled = joining || currentUserSteamId === team.leader?.steamId
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
if (joining) return
|
if (joining) return
|
||||||
|
|||||||
@ -200,7 +200,7 @@ export default function UserMatchesList({ steamId }: { steamId: string }) {
|
|||||||
{matches.map(m => {
|
{matches.map(m => {
|
||||||
const mapInfo =
|
const mapInfo =
|
||||||
MAP_OPTIONS.find(opt => opt.key === m.map) ??
|
MAP_OPTIONS.find(opt => opt.key === m.map) ??
|
||||||
MAP_OPTIONS.find(opt => opt.key === 'lobby_mapveto')
|
MAP_OPTIONS.find(opt => opt.key === 'lobby_mapvote')
|
||||||
const [scoreCT, scoreT] = parseScore(m.score)
|
const [scoreCT, scoreT] = parseScore(m.score)
|
||||||
|
|
||||||
const ownCTSide = m.team !== 'T'
|
const ownCTSide = m.team !== 'T'
|
||||||
|
|||||||
@ -1,25 +1,73 @@
|
|||||||
// src/lib/mapOptions.ts
|
// src/lib/mapOptions.ts
|
||||||
export type MapOption = { key: string; label: string }
|
export type MapOption = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
images: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export const MAP_OPTIONS: MapOption[] = [
|
export const MAP_OPTIONS: MapOption[] = [
|
||||||
{ key: 'de_train', label: 'Train' },
|
{
|
||||||
{ key: 'ar_baggage', label: 'Baggage' },
|
key: 'de_train',
|
||||||
{ key: 'ar_pool_day', label: 'Pool Day' },
|
label: 'Train',
|
||||||
{ key: 'ar_shoots', label: 'Shoots' },
|
images: [
|
||||||
{ key: 'cs_agency', label: 'Agency' },
|
'/assets/img/maps/de_train/1.jpg',
|
||||||
{ key: 'cs_italy', label: 'Italy' },
|
'/assets/img/maps/de_train/2.jpg',
|
||||||
{ key: 'cs_office', label: 'Office' },
|
],
|
||||||
{ key: 'de_ancient', label: 'Ancient' },
|
},
|
||||||
{ key: 'de_anubis', label: 'Anubis' },
|
{
|
||||||
{ key: 'de_brewery', label: 'Brewery' },
|
key: 'de_dust2',
|
||||||
{ key: 'de_dogtown', label: 'Dogtown' },
|
label: 'Dust 2',
|
||||||
{ key: 'de_dust2', label: 'Dust 2' },
|
images: [
|
||||||
{ key: 'de_grail', label: 'Grail' },
|
'/assets/img/maps/de_dust2/1.jpg',
|
||||||
{ key: 'de_inferno', label: 'Inferno' },
|
'/assets/img/maps/de_dust2/2.jpg',
|
||||||
{ key: 'de_jura', label: 'Jura' },
|
],
|
||||||
{ key: 'de_mirage', label: 'Mirage' },
|
},
|
||||||
{ key: 'de_nuke', label: 'Nuke' },
|
{
|
||||||
{ key: 'de_overpass', label: 'Overpass' },
|
key: 'de_mirage',
|
||||||
{ key: 'de_vertigo', label: 'Vertigo' },
|
label: 'Mirage',
|
||||||
{ key: 'lobby_mapveto', label: 'Pick/Ban' },
|
images: [
|
||||||
|
'/assets/img/maps/de_mirage/1.jpg',
|
||||||
|
'/assets/img/maps/de_mirage/2.jpg',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'de_nuke',
|
||||||
|
label: 'Nuke',
|
||||||
|
images: [
|
||||||
|
'/assets/img/maps/de_nuke/1.jpg',
|
||||||
|
'/assets/img/maps/de_nuke/2.jpg',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'de_ancient',
|
||||||
|
label: 'Ancient',
|
||||||
|
images: [
|
||||||
|
'/assets/img/maps/de_ancient/1.jpg',
|
||||||
|
'/assets/img/maps/de_ancient/2.jpg',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'de_inferno',
|
||||||
|
label: 'Inferno',
|
||||||
|
images: [
|
||||||
|
'/assets/img/maps/de_inferno/1.jpg',
|
||||||
|
'/assets/img/maps/de_inferno/2.jpg',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'de_overpass',
|
||||||
|
label: 'Overpass',
|
||||||
|
images: [
|
||||||
|
'/assets/img/maps/de_overpass/1.jpg',
|
||||||
|
'/assets/img/maps/de_overpass/2.jpg',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lobby_mapvote',
|
||||||
|
label: 'Pick/Ban',
|
||||||
|
images: [
|
||||||
|
'/assets/img/maps/lobby_mapvote/1.jpg',
|
||||||
|
'/assets/img/maps/lobby_mapvote/2.jpg',
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -20,12 +20,11 @@ export const SSE_EVENT_TYPES = [
|
|||||||
'expired-sharecode',
|
'expired-sharecode',
|
||||||
'team-invite-revoked',
|
'team-invite-revoked',
|
||||||
'map-vote-updated',
|
'map-vote-updated',
|
||||||
|
'map-vote-admin-edit',
|
||||||
'match-created',
|
'match-created',
|
||||||
'matches-updated',
|
'matches-updated',
|
||||||
'match-deleted',
|
'match-deleted',
|
||||||
'match-updated',
|
'match-updated',
|
||||||
|
|
||||||
// ➕ neu: gezieltes Event, wenn sich die Aufstellung ändert
|
|
||||||
'match-lineup-updated',
|
'match-lineup-updated',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@ -67,6 +66,8 @@ export const MATCH_EVENTS: ReadonlySet<SSEEventType> = new Set([
|
|||||||
'match-deleted',
|
'match-deleted',
|
||||||
'match-lineup-updated',
|
'match-lineup-updated',
|
||||||
'match-updated',
|
'match-updated',
|
||||||
|
'map-vote-updated',
|
||||||
|
'map-vote-admin-edit',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Event-Typen, die das NotificationCenter betreffen
|
// Event-Typen, die das NotificationCenter betreffen
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
// app/match-details/[matchId]/vote/VoteClient.tsx
|
// app/match-details/[matchId]/vote/VoteClient.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import MapVetoPanel from '@/app/components/MapVetoPanel'
|
import MapVotePanel from '@/app/components/MapVotePanel'
|
||||||
import { useMatch } from '../MatchContext' // aus dem Layout-Context
|
import { useMatch } from '../MatchContext' // aus dem Layout-Context
|
||||||
|
|
||||||
export default function VoteClient() {
|
export default function VoteClient() {
|
||||||
const match = useMatch()
|
const match = useMatch()
|
||||||
return <MapVetoPanel match={match} />
|
return <MapVotePanel match={match} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
// /types/mapveto.ts
|
|
||||||
import type { Player } from './team'
|
|
||||||
|
|
||||||
export type MapVetoStep = {
|
|
||||||
order: number
|
|
||||||
action: 'ban'|'pick'|'decider'
|
|
||||||
teamId: string | null
|
|
||||||
map: string | null
|
|
||||||
chosenAt: string | null
|
|
||||||
chosenBy: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MapVetoTeam = {
|
|
||||||
id: string | null
|
|
||||||
name?: string | null
|
|
||||||
logo?: string | null
|
|
||||||
leader: Player | null
|
|
||||||
players: Player[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MapVetoState = {
|
|
||||||
bestOf: number
|
|
||||||
mapPool: string[]
|
|
||||||
currentIndex: number
|
|
||||||
locked: boolean
|
|
||||||
opensAt: string | null
|
|
||||||
steps: MapVetoStep[]
|
|
||||||
teams?: {
|
|
||||||
teamA: MapVetoTeam
|
|
||||||
teamB: MapVetoTeam
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
src/app/types/mapvote.ts
Normal file
50
src/app/types/mapvote.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// /types/mapvote.ts
|
||||||
|
import type { Player } from './team'
|
||||||
|
|
||||||
|
export type MapVoteStep = {
|
||||||
|
order: number
|
||||||
|
action: 'ban'|'pick'|'decider'
|
||||||
|
teamId: string | null
|
||||||
|
map: string | null
|
||||||
|
chosenAt: string | null
|
||||||
|
chosenBy: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MapVoteTeam = {
|
||||||
|
id: string | null
|
||||||
|
name?: string | null
|
||||||
|
logo?: string | null
|
||||||
|
leader: Player | null
|
||||||
|
players: Player[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MapVoteMapVisual = {
|
||||||
|
/** Menschlicher Name aus MAP_OPTIONS */
|
||||||
|
label: string
|
||||||
|
/** Vom Server vorab ausgewähltes Hintergrundbild für diese Map (stabil pro Match) */
|
||||||
|
bg: string
|
||||||
|
/** Optional: alle verfügbaren Bilder (falls du sie im FE brauchst) */
|
||||||
|
images?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⬅️ NEU: Admin-Edit-Metadaten (für globalen Freeze)
|
||||||
|
export type MapVoteAdminEdit = {
|
||||||
|
enabled: boolean
|
||||||
|
by: string | null // steamId des Admins
|
||||||
|
since: string | null // ISO-String
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MapVoteState = {
|
||||||
|
bestOf: number
|
||||||
|
mapPool: string[]
|
||||||
|
mapVisuals: Record<string, MapVoteMapVisual>
|
||||||
|
currentIndex: number
|
||||||
|
locked: boolean
|
||||||
|
opensAt: string | null
|
||||||
|
steps: MapVoteStep[]
|
||||||
|
teams?: {
|
||||||
|
teamA: MapVoteTeam
|
||||||
|
teamB: MapVoteTeam
|
||||||
|
}
|
||||||
|
adminEdit?: MapVoteAdminEdit // ⬅️ NEU
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ export type Match = {
|
|||||||
teamA: Team
|
teamA: Team
|
||||||
teamB: Team
|
teamB: Team
|
||||||
|
|
||||||
mapVeto?: {
|
mapVote?: {
|
||||||
status: 'not_started' | 'in_progress' | 'completed' | null
|
status: 'not_started' | 'in_progress' | 'completed' | null
|
||||||
opensAt: string | null
|
opensAt: string | null
|
||||||
isOpen: boolean | null
|
isOpen: boolean | null
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -20,12 +20,12 @@ exports.Prisma = Prisma
|
|||||||
exports.$Enums = {}
|
exports.$Enums = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prisma Client JS version: 6.13.0
|
* Prisma Client JS version: 6.14.0
|
||||||
* Query Engine version: 361e86d0ea4987e9f53a565309b3eed797a6bcbd
|
* Query Engine version: 717184b7b35ea05dfa71a3236b7af656013e1e49
|
||||||
*/
|
*/
|
||||||
Prisma.prismaVersion = {
|
Prisma.prismaVersion = {
|
||||||
client: "6.13.0",
|
client: "6.14.0",
|
||||||
engine: "361e86d0ea4987e9f53a565309b3eed797a6bcbd"
|
engine: "717184b7b35ea05dfa71a3236b7af656013e1e49"
|
||||||
}
|
}
|
||||||
|
|
||||||
Prisma.PrismaClientKnownRequestError = () => {
|
Prisma.PrismaClientKnownRequestError = () => {
|
||||||
@ -276,7 +276,7 @@ exports.Prisma.ServerRequestScalarFieldEnum = {
|
|||||||
createdAt: 'createdAt'
|
createdAt: 'createdAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Prisma.MapVetoScalarFieldEnum = {
|
exports.Prisma.MapVoteScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
matchId: 'matchId',
|
matchId: 'matchId',
|
||||||
bestOf: 'bestOf',
|
bestOf: 'bestOf',
|
||||||
@ -284,13 +284,15 @@ exports.Prisma.MapVetoScalarFieldEnum = {
|
|||||||
currentIdx: 'currentIdx',
|
currentIdx: 'currentIdx',
|
||||||
locked: 'locked',
|
locked: 'locked',
|
||||||
opensAt: 'opensAt',
|
opensAt: 'opensAt',
|
||||||
|
adminEditingBy: 'adminEditingBy',
|
||||||
|
adminEditingSince: 'adminEditingSince',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Prisma.MapVetoStepScalarFieldEnum = {
|
exports.Prisma.MapVoteStepScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
vetoId: 'vetoId',
|
voteId: 'voteId',
|
||||||
order: 'order',
|
order: 'order',
|
||||||
action: 'action',
|
action: 'action',
|
||||||
teamId: 'teamId',
|
teamId: 'teamId',
|
||||||
@ -332,7 +334,7 @@ exports.ScheduleStatus = exports.$Enums.ScheduleStatus = {
|
|||||||
COMPLETED: 'COMPLETED'
|
COMPLETED: 'COMPLETED'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.MapVetoAction = exports.$Enums.MapVetoAction = {
|
exports.MapVoteAction = exports.$Enums.MapVoteAction = {
|
||||||
BAN: 'BAN',
|
BAN: 'BAN',
|
||||||
PICK: 'PICK',
|
PICK: 'PICK',
|
||||||
DECIDER: 'DECIDER'
|
DECIDER: 'DECIDER'
|
||||||
@ -350,8 +352,8 @@ exports.Prisma.ModelName = {
|
|||||||
Schedule: 'Schedule',
|
Schedule: 'Schedule',
|
||||||
DemoFile: 'DemoFile',
|
DemoFile: 'DemoFile',
|
||||||
ServerRequest: 'ServerRequest',
|
ServerRequest: 'ServerRequest',
|
||||||
MapVeto: 'MapVeto',
|
MapVote: 'MapVote',
|
||||||
MapVetoStep: 'MapVetoStep'
|
MapVoteStep: 'MapVoteStep'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
3260
src/generated/prisma/index.d.ts
vendored
3260
src/generated/prisma/index.d.ts
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-c63ea7016e1a1ac5fd312c9d5648426292d519ae426c4dfab5e695d19cc61ccb",
|
"name": "prisma-client-d6ee9e0758cbf84308223ad2add52d1ebc187831f60d22da54a3cc72e384b3c6",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "index-browser.js",
|
"browser": "index-browser.js",
|
||||||
@ -145,6 +145,6 @@
|
|||||||
},
|
},
|
||||||
"./*": "./*"
|
"./*": "./*"
|
||||||
},
|
},
|
||||||
"version": "6.13.0",
|
"version": "6.14.0",
|
||||||
"sideEffects": false
|
"sideEffects": false
|
||||||
}
|
}
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
561
src/generated/prisma/runtime/library.d.ts
vendored
561
src/generated/prisma/runtime/library.d.ts
vendored
@ -209,7 +209,7 @@ declare const ColumnTypeEnum: {
|
|||||||
|
|
||||||
declare type CompactedBatchResponse = {
|
declare type CompactedBatchResponse = {
|
||||||
type: 'compacted';
|
type: 'compacted';
|
||||||
plan: {};
|
plan: QueryPlanNode;
|
||||||
arguments: Record<string, {}>[];
|
arguments: Record<string, {}>[];
|
||||||
nestedSelection: string[];
|
nestedSelection: string[];
|
||||||
keys: string[];
|
keys: string[];
|
||||||
@ -376,6 +376,19 @@ declare type DatamodelEnum = ReadonlyDeep_2<{
|
|||||||
|
|
||||||
declare function datamodelEnumToSchemaEnum(datamodelEnum: DatamodelEnum): SchemaEnum;
|
declare function datamodelEnumToSchemaEnum(datamodelEnum: DatamodelEnum): SchemaEnum;
|
||||||
|
|
||||||
|
declare type DataRule = {
|
||||||
|
type: 'rowCountEq';
|
||||||
|
args: number;
|
||||||
|
} | {
|
||||||
|
type: 'rowCountNeq';
|
||||||
|
args: number;
|
||||||
|
} | {
|
||||||
|
type: 'affectedRowCountEq';
|
||||||
|
args: number;
|
||||||
|
} | {
|
||||||
|
type: 'never';
|
||||||
|
};
|
||||||
|
|
||||||
declare type Datasource = {
|
declare type Datasource = {
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
@ -709,7 +722,7 @@ export declare function defineDmmfProperty(target: object, runtimeDataModel: Run
|
|||||||
|
|
||||||
declare function defineExtension(ext: ExtensionArgs | ((client: Client) => Client)): (client: Client) => Client;
|
declare function defineExtension(ext: ExtensionArgs | ((client: Client) => Client)): (client: Client) => Client;
|
||||||
|
|
||||||
declare const denylist: readonly ["$connect", "$disconnect", "$on", "$transaction", "$use", "$extends"];
|
declare const denylist: readonly ["$connect", "$disconnect", "$on", "$transaction", "$extends"];
|
||||||
|
|
||||||
declare type Deprecation = ReadonlyDeep_2<{
|
declare type Deprecation = ReadonlyDeep_2<{
|
||||||
sinceVersion: string;
|
sinceVersion: string;
|
||||||
@ -1140,114 +1153,9 @@ declare interface EnvValue {
|
|||||||
|
|
||||||
export declare type Equals<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? 1 : 0;
|
export declare type Equals<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? 1 : 0;
|
||||||
|
|
||||||
declare type Error_2 = {
|
declare type Error_2 = MappedError & {
|
||||||
kind: 'GenericJs';
|
originalCode?: string;
|
||||||
id: number;
|
originalMessage?: string;
|
||||||
} | {
|
|
||||||
kind: 'UnsupportedNativeDataType';
|
|
||||||
type: string;
|
|
||||||
} | {
|
|
||||||
kind: 'InvalidIsolationLevel';
|
|
||||||
level: string;
|
|
||||||
} | {
|
|
||||||
kind: 'LengthMismatch';
|
|
||||||
column?: string;
|
|
||||||
} | {
|
|
||||||
kind: 'UniqueConstraintViolation';
|
|
||||||
constraint?: {
|
|
||||||
fields: string[];
|
|
||||||
} | {
|
|
||||||
index: string;
|
|
||||||
} | {
|
|
||||||
foreignKey: {};
|
|
||||||
};
|
|
||||||
} | {
|
|
||||||
kind: 'NullConstraintViolation';
|
|
||||||
constraint?: {
|
|
||||||
fields: string[];
|
|
||||||
} | {
|
|
||||||
index: string;
|
|
||||||
} | {
|
|
||||||
foreignKey: {};
|
|
||||||
};
|
|
||||||
} | {
|
|
||||||
kind: 'ForeignKeyConstraintViolation';
|
|
||||||
constraint?: {
|
|
||||||
fields: string[];
|
|
||||||
} | {
|
|
||||||
index: string;
|
|
||||||
} | {
|
|
||||||
foreignKey: {};
|
|
||||||
};
|
|
||||||
} | {
|
|
||||||
kind: 'DatabaseNotReachable';
|
|
||||||
host?: string;
|
|
||||||
port?: number;
|
|
||||||
} | {
|
|
||||||
kind: 'DatabaseDoesNotExist';
|
|
||||||
db?: string;
|
|
||||||
} | {
|
|
||||||
kind: 'DatabaseAlreadyExists';
|
|
||||||
db?: string;
|
|
||||||
} | {
|
|
||||||
kind: 'DatabaseAccessDenied';
|
|
||||||
db?: string;
|
|
||||||
} | {
|
|
||||||
kind: 'ConnectionClosed';
|
|
||||||
} | {
|
|
||||||
kind: 'TlsConnectionError';
|
|
||||||
reason: string;
|
|
||||||
} | {
|
|
||||||
kind: 'AuthenticationFailed';
|
|
||||||
user?: string;
|
|
||||||
} | {
|
|
||||||
kind: 'TransactionWriteConflict';
|
|
||||||
} | {
|
|
||||||
kind: 'TableDoesNotExist';
|
|
||||||
table?: string;
|
|
||||||
} | {
|
|
||||||
kind: 'ColumnNotFound';
|
|
||||||
column?: string;
|
|
||||||
} | {
|
|
||||||
kind: 'TooManyConnections';
|
|
||||||
cause: string;
|
|
||||||
} | {
|
|
||||||
kind: 'ValueOutOfRange';
|
|
||||||
cause: string;
|
|
||||||
} | {
|
|
||||||
kind: 'MissingFullTextSearchIndex';
|
|
||||||
} | {
|
|
||||||
kind: 'SocketTimeout';
|
|
||||||
} | {
|
|
||||||
kind: 'InconsistentColumnData';
|
|
||||||
cause: string;
|
|
||||||
} | {
|
|
||||||
kind: 'TransactionAlreadyClosed';
|
|
||||||
cause: string;
|
|
||||||
} | {
|
|
||||||
kind: 'postgres';
|
|
||||||
code: string;
|
|
||||||
severity: string;
|
|
||||||
message: string;
|
|
||||||
detail: string | undefined;
|
|
||||||
column: string | undefined;
|
|
||||||
hint: string | undefined;
|
|
||||||
} | {
|
|
||||||
kind: 'mysql';
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
state: string;
|
|
||||||
} | {
|
|
||||||
kind: 'sqlite';
|
|
||||||
/**
|
|
||||||
* Sqlite extended error code: https://www.sqlite.org/rescode.html
|
|
||||||
*/
|
|
||||||
extendedCode: number;
|
|
||||||
message: string;
|
|
||||||
} | {
|
|
||||||
kind: 'mssql';
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type ErrorCapturingFunction<T> = T extends (...args: infer A) => Promise<infer R> ? (...args: A) => Promise<Result_4<ErrorCapturingInterface<R>>> : T extends (...args: infer A) => infer R ? (...args: A) => Result_4<ErrorCapturingInterface<R>> : T;
|
declare type ErrorCapturingFunction<T> = T extends (...args: infer A) => Promise<infer R> ? (...args: A) => Promise<Result_4<ErrorCapturingInterface<R>>> : T extends (...args: infer A) => infer R ? (...args: A) => Result_4<ErrorCapturingInterface<R>> : T;
|
||||||
@ -1314,7 +1222,6 @@ declare type ExtendedSpanOptions = SpanOptions & {
|
|||||||
/** The name of the span */
|
/** The name of the span */
|
||||||
name: string;
|
name: string;
|
||||||
internal?: boolean;
|
internal?: boolean;
|
||||||
middleware?: boolean;
|
|
||||||
/** Whether it propagates context (?=true) */
|
/** Whether it propagates context (?=true) */
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
/** The context to append the span to */
|
/** The context to append the span to */
|
||||||
@ -1452,12 +1359,36 @@ declare type FieldDefault = ReadonlyDeep_2<{
|
|||||||
|
|
||||||
declare type FieldDefaultScalar = string | boolean | number;
|
declare type FieldDefaultScalar = string | boolean | number;
|
||||||
|
|
||||||
|
declare type FieldInitializer = {
|
||||||
|
type: 'value';
|
||||||
|
value: PrismaValue;
|
||||||
|
} | {
|
||||||
|
type: 'lastInsertId';
|
||||||
|
};
|
||||||
|
|
||||||
declare type FieldKind = 'scalar' | 'object' | 'enum' | 'unsupported';
|
declare type FieldKind = 'scalar' | 'object' | 'enum' | 'unsupported';
|
||||||
|
|
||||||
declare type FieldLocation = 'scalar' | 'inputObjectTypes' | 'outputObjectTypes' | 'enumTypes' | 'fieldRefTypes';
|
declare type FieldLocation = 'scalar' | 'inputObjectTypes' | 'outputObjectTypes' | 'enumTypes' | 'fieldRefTypes';
|
||||||
|
|
||||||
declare type FieldNamespace = 'model' | 'prisma';
|
declare type FieldNamespace = 'model' | 'prisma';
|
||||||
|
|
||||||
|
declare type FieldOperation = {
|
||||||
|
type: 'set';
|
||||||
|
value: PrismaValue;
|
||||||
|
} | {
|
||||||
|
type: 'add';
|
||||||
|
value: PrismaValue;
|
||||||
|
} | {
|
||||||
|
type: 'subtract';
|
||||||
|
value: PrismaValue;
|
||||||
|
} | {
|
||||||
|
type: 'multiply';
|
||||||
|
value: PrismaValue;
|
||||||
|
} | {
|
||||||
|
type: 'divide';
|
||||||
|
value: PrismaValue;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reference to a specific field of a specific model
|
* A reference to a specific field of a specific model
|
||||||
*/
|
*/
|
||||||
@ -1483,6 +1414,21 @@ export declare interface Fn<Params = unknown, Returns = unknown> {
|
|||||||
returns: Returns;
|
returns: Returns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare type Fragment = {
|
||||||
|
type: 'stringChunk';
|
||||||
|
chunk: string;
|
||||||
|
} | {
|
||||||
|
type: 'parameter';
|
||||||
|
} | {
|
||||||
|
type: 'parameterTuple';
|
||||||
|
} | {
|
||||||
|
type: 'parameterTupleList';
|
||||||
|
itemPrefix: string;
|
||||||
|
itemSeparator: string;
|
||||||
|
itemSuffix: string;
|
||||||
|
groupSeparator: string;
|
||||||
|
};
|
||||||
|
|
||||||
declare interface GeneratorConfig {
|
declare interface GeneratorConfig {
|
||||||
name: string;
|
name: string;
|
||||||
output: EnvValue | null;
|
output: EnvValue | null;
|
||||||
@ -1576,7 +1522,6 @@ export declare function getPrismaClient(config: GetPrismaClientConfig): {
|
|||||||
_clientVersion: string;
|
_clientVersion: string;
|
||||||
_errorFormat: ErrorFormat;
|
_errorFormat: ErrorFormat;
|
||||||
_tracingHelper: TracingHelper;
|
_tracingHelper: TracingHelper;
|
||||||
_middlewares: MiddlewareHandler<QueryMiddleware>;
|
|
||||||
_previewFeatures: string[];
|
_previewFeatures: string[];
|
||||||
_activeProvider: string;
|
_activeProvider: string;
|
||||||
_globalOmit?: GlobalOmitOptions | undefined;
|
_globalOmit?: GlobalOmitOptions | undefined;
|
||||||
@ -1591,11 +1536,6 @@ export declare function getPrismaClient(config: GetPrismaClientConfig): {
|
|||||||
*/
|
*/
|
||||||
_appliedParent: any;
|
_appliedParent: any;
|
||||||
_createPrismaPromise: PrismaPromiseFactory;
|
_createPrismaPromise: PrismaPromiseFactory;
|
||||||
/**
|
|
||||||
* Hook a middleware into the client
|
|
||||||
* @param middleware to hook
|
|
||||||
*/
|
|
||||||
$use(middleware: QueryMiddleware): void;
|
|
||||||
$on<E extends ExtendedEventType>(eventType: E, callback: EventCallback<E>): any;
|
$on<E extends ExtendedEventType>(eventType: E, callback: EventCallback<E>): any;
|
||||||
$connect(): Promise<void>;
|
$connect(): Promise<void>;
|
||||||
/**
|
/**
|
||||||
@ -1875,6 +1815,14 @@ declare type IndexField = ReadonlyDeep_2<{
|
|||||||
|
|
||||||
declare type IndexType = 'id' | 'normal' | 'unique' | 'fulltext';
|
declare type IndexType = 'id' | 'normal' | 'unique' | 'fulltext';
|
||||||
|
|
||||||
|
declare type InMemoryOps = {
|
||||||
|
pagination: Pagination | null;
|
||||||
|
distinct: string[] | null;
|
||||||
|
reverse: boolean;
|
||||||
|
linkingFields: string[] | null;
|
||||||
|
nested: Record<string, InMemoryOps>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matches a JSON array.
|
* Matches a JSON array.
|
||||||
* Unlike \`JsonArray\`, readonly arrays are assignable to this type.
|
* Unlike \`JsonArray\`, readonly arrays are assignable to this type.
|
||||||
@ -2021,6 +1969,13 @@ declare interface Job {
|
|||||||
*/
|
*/
|
||||||
export declare function join(values: readonly RawValue[], separator?: string, prefix?: string, suffix?: string): Sql;
|
export declare function join(values: readonly RawValue[], separator?: string, prefix?: string, suffix?: string): Sql;
|
||||||
|
|
||||||
|
declare type JoinExpression = {
|
||||||
|
child: QueryPlanNode;
|
||||||
|
on: [left: string, right: string][];
|
||||||
|
parentField: string;
|
||||||
|
isRelationUnique: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export declare type JsArgs = {
|
export declare type JsArgs = {
|
||||||
select?: Selection_2;
|
select?: Selection_2;
|
||||||
include?: Selection_2;
|
include?: Selection_2;
|
||||||
@ -2190,6 +2145,116 @@ export declare function makeStrictEnum<T extends Record<PropertyKey, string | nu
|
|||||||
|
|
||||||
export declare function makeTypedQueryFactory(sql: string): (...values: any[]) => TypedSql<any[], unknown>;
|
export declare function makeTypedQueryFactory(sql: string): (...values: any[]) => TypedSql<any[], unknown>;
|
||||||
|
|
||||||
|
declare type MappedError = {
|
||||||
|
kind: 'GenericJs';
|
||||||
|
id: number;
|
||||||
|
} | {
|
||||||
|
kind: 'UnsupportedNativeDataType';
|
||||||
|
type: string;
|
||||||
|
} | {
|
||||||
|
kind: 'InvalidIsolationLevel';
|
||||||
|
level: string;
|
||||||
|
} | {
|
||||||
|
kind: 'LengthMismatch';
|
||||||
|
column?: string;
|
||||||
|
} | {
|
||||||
|
kind: 'UniqueConstraintViolation';
|
||||||
|
constraint?: {
|
||||||
|
fields: string[];
|
||||||
|
} | {
|
||||||
|
index: string;
|
||||||
|
} | {
|
||||||
|
foreignKey: {};
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
kind: 'NullConstraintViolation';
|
||||||
|
constraint?: {
|
||||||
|
fields: string[];
|
||||||
|
} | {
|
||||||
|
index: string;
|
||||||
|
} | {
|
||||||
|
foreignKey: {};
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
kind: 'ForeignKeyConstraintViolation';
|
||||||
|
constraint?: {
|
||||||
|
fields: string[];
|
||||||
|
} | {
|
||||||
|
index: string;
|
||||||
|
} | {
|
||||||
|
foreignKey: {};
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
kind: 'DatabaseNotReachable';
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
} | {
|
||||||
|
kind: 'DatabaseDoesNotExist';
|
||||||
|
db?: string;
|
||||||
|
} | {
|
||||||
|
kind: 'DatabaseAlreadyExists';
|
||||||
|
db?: string;
|
||||||
|
} | {
|
||||||
|
kind: 'DatabaseAccessDenied';
|
||||||
|
db?: string;
|
||||||
|
} | {
|
||||||
|
kind: 'ConnectionClosed';
|
||||||
|
} | {
|
||||||
|
kind: 'TlsConnectionError';
|
||||||
|
reason: string;
|
||||||
|
} | {
|
||||||
|
kind: 'AuthenticationFailed';
|
||||||
|
user?: string;
|
||||||
|
} | {
|
||||||
|
kind: 'TransactionWriteConflict';
|
||||||
|
} | {
|
||||||
|
kind: 'TableDoesNotExist';
|
||||||
|
table?: string;
|
||||||
|
} | {
|
||||||
|
kind: 'ColumnNotFound';
|
||||||
|
column?: string;
|
||||||
|
} | {
|
||||||
|
kind: 'TooManyConnections';
|
||||||
|
cause: string;
|
||||||
|
} | {
|
||||||
|
kind: 'ValueOutOfRange';
|
||||||
|
cause: string;
|
||||||
|
} | {
|
||||||
|
kind: 'MissingFullTextSearchIndex';
|
||||||
|
} | {
|
||||||
|
kind: 'SocketTimeout';
|
||||||
|
} | {
|
||||||
|
kind: 'InconsistentColumnData';
|
||||||
|
cause: string;
|
||||||
|
} | {
|
||||||
|
kind: 'TransactionAlreadyClosed';
|
||||||
|
cause: string;
|
||||||
|
} | {
|
||||||
|
kind: 'postgres';
|
||||||
|
code: string;
|
||||||
|
severity: string;
|
||||||
|
message: string;
|
||||||
|
detail: string | undefined;
|
||||||
|
column: string | undefined;
|
||||||
|
hint: string | undefined;
|
||||||
|
} | {
|
||||||
|
kind: 'mysql';
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
state: string;
|
||||||
|
} | {
|
||||||
|
kind: 'sqlite';
|
||||||
|
/**
|
||||||
|
* Sqlite extended error code: https://www.sqlite.org/rescode.html
|
||||||
|
*/
|
||||||
|
extendedCode: number;
|
||||||
|
message: string;
|
||||||
|
} | {
|
||||||
|
kind: 'mssql';
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
declare type Mappings = ReadonlyDeep_2<{
|
declare type Mappings = ReadonlyDeep_2<{
|
||||||
modelOperations: ModelMapping[];
|
modelOperations: ModelMapping[];
|
||||||
otherOperations: {
|
otherOperations: {
|
||||||
@ -2289,14 +2354,6 @@ declare type MiddlewareArgsMapper<RequestArgs, MiddlewareArgs> = {
|
|||||||
middlewareArgsToRequestArgs(middlewareArgs: MiddlewareArgs): RequestArgs;
|
middlewareArgsToRequestArgs(middlewareArgs: MiddlewareArgs): RequestArgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare class MiddlewareHandler<M extends Function> {
|
|
||||||
private _middlewares;
|
|
||||||
use(middleware: M): void;
|
|
||||||
get(id: number): M | undefined;
|
|
||||||
has(id: number): boolean;
|
|
||||||
length(): number;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare type Model = ReadonlyDeep_2<{
|
declare type Model = ReadonlyDeep_2<{
|
||||||
name: string;
|
name: string;
|
||||||
dbName: string | null;
|
dbName: string | null;
|
||||||
@ -2378,7 +2435,7 @@ export declare type ModelQueryOptionsCbArgs = {
|
|||||||
|
|
||||||
declare type MultiBatchResponse = {
|
declare type MultiBatchResponse = {
|
||||||
type: 'multi';
|
type: 'multi';
|
||||||
plans: object[];
|
plans: QueryPlanNode[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type NameArgs = {
|
export declare type NameArgs = {
|
||||||
@ -2496,6 +2553,12 @@ declare type OutputType = ReadonlyDeep_2<{
|
|||||||
|
|
||||||
declare type OutputTypeRef = TypeRef<'scalar' | 'outputObjectTypes' | 'enumTypes'>;
|
declare type OutputTypeRef = TypeRef<'scalar' | 'outputObjectTypes' | 'enumTypes'>;
|
||||||
|
|
||||||
|
declare type Pagination = {
|
||||||
|
cursor: Record<string, PrismaValue> | null;
|
||||||
|
take: number | null;
|
||||||
|
skip: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export declare function Param<$Type, $Value extends string>(name: $Value): Param<$Type, $Value>;
|
export declare function Param<$Type, $Value extends string>(name: $Value): Param<$Type, $Value>;
|
||||||
|
|
||||||
export declare type Param<out $Type, $Value extends string> = {
|
export declare type Param<out $Type, $Value extends string> = {
|
||||||
@ -2523,6 +2586,11 @@ declare type Pick_2<T, K extends string | number | symbol> = {
|
|||||||
};
|
};
|
||||||
export { Pick_2 as Pick }
|
export { Pick_2 as Pick }
|
||||||
|
|
||||||
|
declare interface PlaceholderFormat {
|
||||||
|
prefix: string;
|
||||||
|
hasNumbering: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
declare type PrimaryKey = ReadonlyDeep_2<{
|
declare type PrimaryKey = ReadonlyDeep_2<{
|
||||||
name: string | null;
|
name: string | null;
|
||||||
fields: string[];
|
fields: string[];
|
||||||
@ -2696,6 +2764,66 @@ declare type PrismaPromiseInteractiveTransaction<PayloadType = unknown> = {
|
|||||||
|
|
||||||
declare type PrismaPromiseTransaction<PayloadType = unknown> = PrismaPromiseBatchTransaction | PrismaPromiseInteractiveTransaction<PayloadType>;
|
declare type PrismaPromiseTransaction<PayloadType = unknown> = PrismaPromiseBatchTransaction | PrismaPromiseInteractiveTransaction<PayloadType>;
|
||||||
|
|
||||||
|
declare type PrismaValue = string | boolean | number | PrismaValue[] | null | Record<string, unknown> | PrismaValuePlaceholder | PrismaValueGenerator | PrismaValueBytes | PrismaValueBigInt;
|
||||||
|
|
||||||
|
declare type PrismaValueBigInt = {
|
||||||
|
prisma__type: 'bigint';
|
||||||
|
prisma__value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type PrismaValueBytes = {
|
||||||
|
prisma__type: 'bytes';
|
||||||
|
prisma__value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type PrismaValueGenerator = {
|
||||||
|
prisma__type: 'generatorCall';
|
||||||
|
prisma__value: {
|
||||||
|
name: string;
|
||||||
|
args: PrismaValue[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type PrismaValuePlaceholder = {
|
||||||
|
prisma__type: 'param';
|
||||||
|
prisma__value: {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type PrismaValueType = {
|
||||||
|
type: 'Any';
|
||||||
|
} | {
|
||||||
|
type: 'String';
|
||||||
|
} | {
|
||||||
|
type: 'Int';
|
||||||
|
} | {
|
||||||
|
type: 'BigInt';
|
||||||
|
} | {
|
||||||
|
type: 'Float';
|
||||||
|
} | {
|
||||||
|
type: 'Boolean';
|
||||||
|
} | {
|
||||||
|
type: 'Decimal';
|
||||||
|
} | {
|
||||||
|
type: 'Date';
|
||||||
|
} | {
|
||||||
|
type: 'Time';
|
||||||
|
} | {
|
||||||
|
type: 'Array';
|
||||||
|
inner: PrismaValueType;
|
||||||
|
} | {
|
||||||
|
type: 'Json';
|
||||||
|
} | {
|
||||||
|
type: 'Object';
|
||||||
|
} | {
|
||||||
|
type: 'Bytes';
|
||||||
|
} | {
|
||||||
|
type: 'Enum';
|
||||||
|
inner: string;
|
||||||
|
};
|
||||||
|
|
||||||
export declare const PrivateResultType: unique symbol;
|
export declare const PrivateResultType: unique symbol;
|
||||||
|
|
||||||
declare type Provider = 'mysql' | 'postgres' | 'sqlite' | 'sqlserver';
|
declare type Provider = 'mysql' | 'postgres' | 'sqlite' | 'sqlserver';
|
||||||
@ -2822,8 +2950,6 @@ declare type QueryEventType = 'query';
|
|||||||
|
|
||||||
declare type QueryIntrospectionBuiltinType = 'int' | 'bigint' | 'float' | 'double' | 'string' | 'enum' | 'bytes' | 'bool' | 'char' | 'decimal' | 'json' | 'xml' | 'uuid' | 'datetime' | 'date' | 'time' | 'int-array' | 'bigint-array' | 'float-array' | 'double-array' | 'string-array' | 'char-array' | 'bytes-array' | 'bool-array' | 'decimal-array' | 'json-array' | 'xml-array' | 'uuid-array' | 'datetime-array' | 'date-array' | 'time-array' | 'null' | 'unknown';
|
declare type QueryIntrospectionBuiltinType = 'int' | 'bigint' | 'float' | 'double' | 'string' | 'enum' | 'bytes' | 'bool' | 'char' | 'decimal' | 'json' | 'xml' | 'uuid' | 'datetime' | 'date' | 'time' | 'int-array' | 'bigint-array' | 'float-array' | 'double-array' | 'string-array' | 'char-array' | 'bytes-array' | 'bool-array' | 'decimal-array' | 'json-array' | 'xml-array' | 'uuid-array' | 'datetime-array' | 'date-array' | 'time-array' | 'null' | 'unknown';
|
||||||
|
|
||||||
declare type QueryMiddleware = (params: QueryMiddlewareParams, next: (params: QueryMiddlewareParams) => Promise<unknown>) => Promise<unknown>;
|
|
||||||
|
|
||||||
declare type QueryMiddlewareParams = {
|
declare type QueryMiddlewareParams = {
|
||||||
/** The model this is executed on */
|
/** The model this is executed on */
|
||||||
model?: string;
|
model?: string;
|
||||||
@ -2859,6 +2985,130 @@ declare type QueryOutput = ReadonlyDeep_2<{
|
|||||||
isList: boolean;
|
isList: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
declare type QueryPlanBinding = {
|
||||||
|
name: string;
|
||||||
|
expr: QueryPlanNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type QueryPlanDbQuery = {
|
||||||
|
type: 'rawSql';
|
||||||
|
sql: string;
|
||||||
|
params: PrismaValue[];
|
||||||
|
} | {
|
||||||
|
type: 'templateSql';
|
||||||
|
fragments: Fragment[];
|
||||||
|
placeholderFormat: PlaceholderFormat;
|
||||||
|
params: PrismaValue[];
|
||||||
|
chunkable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type QueryPlanNode = {
|
||||||
|
type: 'value';
|
||||||
|
args: PrismaValue;
|
||||||
|
} | {
|
||||||
|
type: 'seq';
|
||||||
|
args: QueryPlanNode[];
|
||||||
|
} | {
|
||||||
|
type: 'get';
|
||||||
|
args: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'let';
|
||||||
|
args: {
|
||||||
|
bindings: QueryPlanBinding[];
|
||||||
|
expr: QueryPlanNode;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'getFirstNonEmpty';
|
||||||
|
args: {
|
||||||
|
names: string[];
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'query';
|
||||||
|
args: QueryPlanDbQuery;
|
||||||
|
} | {
|
||||||
|
type: 'execute';
|
||||||
|
args: QueryPlanDbQuery;
|
||||||
|
} | {
|
||||||
|
type: 'reverse';
|
||||||
|
args: QueryPlanNode;
|
||||||
|
} | {
|
||||||
|
type: 'sum';
|
||||||
|
args: QueryPlanNode[];
|
||||||
|
} | {
|
||||||
|
type: 'concat';
|
||||||
|
args: QueryPlanNode[];
|
||||||
|
} | {
|
||||||
|
type: 'unique';
|
||||||
|
args: QueryPlanNode;
|
||||||
|
} | {
|
||||||
|
type: 'required';
|
||||||
|
args: QueryPlanNode;
|
||||||
|
} | {
|
||||||
|
type: 'join';
|
||||||
|
args: {
|
||||||
|
parent: QueryPlanNode;
|
||||||
|
children: JoinExpression[];
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'mapField';
|
||||||
|
args: {
|
||||||
|
field: string;
|
||||||
|
records: QueryPlanNode;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'transaction';
|
||||||
|
args: QueryPlanNode;
|
||||||
|
} | {
|
||||||
|
type: 'dataMap';
|
||||||
|
args: {
|
||||||
|
expr: QueryPlanNode;
|
||||||
|
structure: ResultNode;
|
||||||
|
enums: Record<string, Record<string, string>>;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'validate';
|
||||||
|
args: {
|
||||||
|
expr: QueryPlanNode;
|
||||||
|
rules: DataRule[];
|
||||||
|
} & ValidationError;
|
||||||
|
} | {
|
||||||
|
type: 'if';
|
||||||
|
args: {
|
||||||
|
value: QueryPlanNode;
|
||||||
|
rule: DataRule;
|
||||||
|
then: QueryPlanNode;
|
||||||
|
else: QueryPlanNode;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'unit';
|
||||||
|
} | {
|
||||||
|
type: 'diff';
|
||||||
|
args: {
|
||||||
|
from: QueryPlanNode;
|
||||||
|
to: QueryPlanNode;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'initializeRecord';
|
||||||
|
args: {
|
||||||
|
expr: QueryPlanNode;
|
||||||
|
fields: Record<string, FieldInitializer>;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'mapRecord';
|
||||||
|
args: {
|
||||||
|
expr: QueryPlanNode;
|
||||||
|
fields: Record<string, FieldOperation>;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
type: 'process';
|
||||||
|
args: {
|
||||||
|
expr: QueryPlanNode;
|
||||||
|
operations: InMemoryOps;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create raw SQL statement.
|
* Create raw SQL statement.
|
||||||
*/
|
*/
|
||||||
@ -3047,6 +3297,19 @@ export declare type ResultFieldDefinition = {
|
|||||||
compute: ResultArgsFieldCompute;
|
compute: ResultArgsFieldCompute;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare type ResultNode = {
|
||||||
|
type: 'AffectedRows';
|
||||||
|
} | {
|
||||||
|
type: 'Object';
|
||||||
|
fields: Record<string, ResultNode>;
|
||||||
|
serializedName: string | null;
|
||||||
|
skipNulls: boolean;
|
||||||
|
} | {
|
||||||
|
type: 'Value';
|
||||||
|
dbName: string;
|
||||||
|
resultType: PrismaValueType;
|
||||||
|
};
|
||||||
|
|
||||||
export declare type Return<T> = T extends (...args: any[]) => infer R ? R : T;
|
export declare type Return<T> = T extends (...args: any[]) => infer R ? R : T;
|
||||||
|
|
||||||
export declare type RuntimeDataModel = {
|
export declare type RuntimeDataModel = {
|
||||||
@ -3679,6 +3942,48 @@ declare namespace Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare type ValidationError = {
|
||||||
|
error_identifier: 'RELATION_VIOLATION';
|
||||||
|
context: {
|
||||||
|
relation: string;
|
||||||
|
modelA: string;
|
||||||
|
modelB: string;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
error_identifier: 'MISSING_RELATED_RECORD';
|
||||||
|
context: {
|
||||||
|
model: string;
|
||||||
|
relation: string;
|
||||||
|
relationType: string;
|
||||||
|
operation: string;
|
||||||
|
neededFor?: string;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
error_identifier: 'MISSING_RECORD';
|
||||||
|
context: {
|
||||||
|
operation: string;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
error_identifier: 'INCOMPLETE_CONNECT_INPUT';
|
||||||
|
context: {
|
||||||
|
expectedRows: number;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
error_identifier: 'INCOMPLETE_CONNECT_OUTPUT';
|
||||||
|
context: {
|
||||||
|
expectedRows: number;
|
||||||
|
relation: string;
|
||||||
|
relationType: string;
|
||||||
|
};
|
||||||
|
} | {
|
||||||
|
error_identifier: 'RECORDS_NOT_CONNECTED';
|
||||||
|
context: {
|
||||||
|
relation: string;
|
||||||
|
parent: string;
|
||||||
|
child: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
|
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
|
||||||
|
|
||||||
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
|
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
56
src/generated/prisma/runtime/react-native.js
vendored
56
src/generated/prisma/runtime/react-native.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -44,7 +44,7 @@ model User {
|
|||||||
createdSchedules Schedule[] @relation("CreatedSchedules")
|
createdSchedules Schedule[] @relation("CreatedSchedules")
|
||||||
confirmedSchedules Schedule[] @relation("ConfirmedSchedules")
|
confirmedSchedules Schedule[] @relation("ConfirmedSchedules")
|
||||||
|
|
||||||
mapVetoChoices MapVetoStep[] @relation("VetoStepChooser")
|
mapVoteChoices MapVoteStep[] @relation("VoteStepChooser")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Team {
|
model Team {
|
||||||
@ -68,7 +68,7 @@ model Team {
|
|||||||
schedulesAsTeamA Schedule[] @relation("ScheduleTeamA")
|
schedulesAsTeamA Schedule[] @relation("ScheduleTeamA")
|
||||||
schedulesAsTeamB Schedule[] @relation("ScheduleTeamB")
|
schedulesAsTeamB Schedule[] @relation("ScheduleTeamB")
|
||||||
|
|
||||||
mapVetoSteps MapVetoStep[] @relation("VetoStepTeam")
|
mapVoteSteps MapVoteStep[] @relation("VoteStepTeam")
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamInvite {
|
model TeamInvite {
|
||||||
@ -138,7 +138,7 @@ model Match {
|
|||||||
|
|
||||||
bestOf Int @default(3) // 1 | 3 | 5 – app-seitig validieren
|
bestOf Int @default(3) // 1 | 3 | 5 – app-seitig validieren
|
||||||
matchDate DateTime? // geplante Startzeit (separat von demoDate)
|
matchDate DateTime? // geplante Startzeit (separat von demoDate)
|
||||||
mapVeto MapVeto? // 1:1 Map-Vote-Status
|
mapVote MapVote?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@ -297,51 +297,49 @@ model ServerRequest {
|
|||||||
// 🗺️ Map-Vote
|
// 🗺️ Map-Vote
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
enum MapVetoAction {
|
enum MapVoteAction {
|
||||||
BAN
|
BAN
|
||||||
PICK
|
PICK
|
||||||
DECIDER
|
DECIDER
|
||||||
}
|
}
|
||||||
|
|
||||||
model MapVeto {
|
model MapVote {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
matchId String @unique
|
matchId String @unique
|
||||||
match Match @relation(fields: [matchId], references: [id])
|
match Match @relation(fields: [matchId], references: [id])
|
||||||
|
|
||||||
// Basiszustand
|
bestOf Int @default(3)
|
||||||
bestOf Int @default(3)
|
mapPool String[]
|
||||||
mapPool String[] // z.B. ["de_inferno","de_mirage",...]
|
currentIdx Int @default(0)
|
||||||
currentIdx Int @default(0)
|
locked Boolean @default(false)
|
||||||
locked Boolean @default(false)
|
opensAt DateTime?
|
||||||
|
|
||||||
// Optional: serverseitig speichern, statt im UI zu berechnen
|
adminEditingBy String?
|
||||||
opensAt DateTime?
|
adminEditingSince DateTime?
|
||||||
|
|
||||||
steps MapVetoStep[]
|
steps MapVoteStep[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model MapVetoStep {
|
model MapVoteStep {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
vetoId String
|
voteId String
|
||||||
order Int
|
order Int
|
||||||
action MapVetoAction
|
action MapVoteAction
|
||||||
|
|
||||||
// Team, das am Zug ist (kann bei DECIDER null sein)
|
|
||||||
teamId String?
|
teamId String?
|
||||||
team Team? @relation("VetoStepTeam", fields: [teamId], references: [id])
|
team Team? @relation("VoteStepTeam", fields: [teamId], references: [id])
|
||||||
|
|
||||||
// Ergebnis & wer gewählt hat
|
|
||||||
map String?
|
map String?
|
||||||
chosenAt DateTime?
|
chosenAt DateTime?
|
||||||
chosenBy String?
|
chosenBy String?
|
||||||
chooser User? @relation("VetoStepChooser", fields: [chosenBy], references: [steamId])
|
chooser User? @relation("VoteStepChooser", fields: [chosenBy], references: [steamId])
|
||||||
|
|
||||||
veto MapVeto @relation(fields: [vetoId], references: [id])
|
vote MapVote @relation(fields: [voteId], references: [id])
|
||||||
|
|
||||||
@@unique([vetoId, order])
|
@@unique([voteId, order])
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
@@index([chosenBy])
|
@@index([chosenBy])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,12 +20,12 @@ exports.Prisma = Prisma
|
|||||||
exports.$Enums = {}
|
exports.$Enums = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prisma Client JS version: 6.13.0
|
* Prisma Client JS version: 6.14.0
|
||||||
* Query Engine version: 361e86d0ea4987e9f53a565309b3eed797a6bcbd
|
* Query Engine version: 717184b7b35ea05dfa71a3236b7af656013e1e49
|
||||||
*/
|
*/
|
||||||
Prisma.prismaVersion = {
|
Prisma.prismaVersion = {
|
||||||
client: "6.13.0",
|
client: "6.14.0",
|
||||||
engine: "361e86d0ea4987e9f53a565309b3eed797a6bcbd"
|
engine: "717184b7b35ea05dfa71a3236b7af656013e1e49"
|
||||||
}
|
}
|
||||||
|
|
||||||
Prisma.PrismaClientKnownRequestError = () => {
|
Prisma.PrismaClientKnownRequestError = () => {
|
||||||
@ -276,7 +276,7 @@ exports.Prisma.ServerRequestScalarFieldEnum = {
|
|||||||
createdAt: 'createdAt'
|
createdAt: 'createdAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Prisma.MapVetoScalarFieldEnum = {
|
exports.Prisma.MapVoteScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
matchId: 'matchId',
|
matchId: 'matchId',
|
||||||
bestOf: 'bestOf',
|
bestOf: 'bestOf',
|
||||||
@ -284,13 +284,15 @@ exports.Prisma.MapVetoScalarFieldEnum = {
|
|||||||
currentIdx: 'currentIdx',
|
currentIdx: 'currentIdx',
|
||||||
locked: 'locked',
|
locked: 'locked',
|
||||||
opensAt: 'opensAt',
|
opensAt: 'opensAt',
|
||||||
|
adminEditingBy: 'adminEditingBy',
|
||||||
|
adminEditingSince: 'adminEditingSince',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Prisma.MapVetoStepScalarFieldEnum = {
|
exports.Prisma.MapVoteStepScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
vetoId: 'vetoId',
|
voteId: 'voteId',
|
||||||
order: 'order',
|
order: 'order',
|
||||||
action: 'action',
|
action: 'action',
|
||||||
teamId: 'teamId',
|
teamId: 'teamId',
|
||||||
@ -332,7 +334,7 @@ exports.ScheduleStatus = exports.$Enums.ScheduleStatus = {
|
|||||||
COMPLETED: 'COMPLETED'
|
COMPLETED: 'COMPLETED'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.MapVetoAction = exports.$Enums.MapVetoAction = {
|
exports.MapVoteAction = exports.$Enums.MapVoteAction = {
|
||||||
BAN: 'BAN',
|
BAN: 'BAN',
|
||||||
PICK: 'PICK',
|
PICK: 'PICK',
|
||||||
DECIDER: 'DECIDER'
|
DECIDER: 'DECIDER'
|
||||||
@ -350,8 +352,8 @@ exports.Prisma.ModelName = {
|
|||||||
Schedule: 'Schedule',
|
Schedule: 'Schedule',
|
||||||
DemoFile: 'DemoFile',
|
DemoFile: 'DemoFile',
|
||||||
ServerRequest: 'ServerRequest',
|
ServerRequest: 'ServerRequest',
|
||||||
MapVeto: 'MapVeto',
|
MapVote: 'MapVote',
|
||||||
MapVetoStep: 'MapVetoStep'
|
MapVoteStep: 'MapVoteStep'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user