updated
This commit is contained in:
parent
f1773a0924
commit
728b5cb6f6
78
package-lock.json
generated
78
package-lock.json
generated
@ -15,7 +15,7 @@
|
||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||
"@preline/dropdown": "^3.0.1",
|
||||
"@preline/tooltip": "^3.0.0",
|
||||
"@prisma/client": "^6.14.0",
|
||||
"@prisma/client": "^6.15.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"csgo-sharecode": "^3.1.2",
|
||||
@ -61,7 +61,7 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.0",
|
||||
"prisma": "^6.14.0",
|
||||
"prisma": "^6.15.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.19.4",
|
||||
@ -1599,9 +1599,9 @@
|
||||
"license": "Licensed under MIT and Preline UI Fair Use License"
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.14.0.tgz",
|
||||
"integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.15.0.tgz",
|
||||
"integrity": "sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@ -1621,9 +1621,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.14.0.tgz",
|
||||
"integrity": "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.15.0.tgz",
|
||||
"integrity": "sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@ -1634,53 +1634,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.14.0.tgz",
|
||||
"integrity": "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.15.0.tgz",
|
||||
"integrity": "sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.14.0.tgz",
|
||||
"integrity": "sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.15.0.tgz",
|
||||
"integrity": "sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.14.0",
|
||||
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||
"@prisma/fetch-engine": "6.14.0",
|
||||
"@prisma/get-platform": "6.14.0"
|
||||
"@prisma/debug": "6.15.0",
|
||||
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
|
||||
"@prisma/fetch-engine": "6.15.0",
|
||||
"@prisma/get-platform": "6.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz",
|
||||
"integrity": "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==",
|
||||
"version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb.tgz",
|
||||
"integrity": "sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz",
|
||||
"integrity": "sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.15.0.tgz",
|
||||
"integrity": "sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.14.0",
|
||||
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||
"@prisma/get-platform": "6.14.0"
|
||||
"@prisma/debug": "6.15.0",
|
||||
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
|
||||
"@prisma/get-platform": "6.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.14.0.tgz",
|
||||
"integrity": "sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.15.0.tgz",
|
||||
"integrity": "sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.14.0"
|
||||
"@prisma/debug": "6.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
@ -6663,9 +6663,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -6781,15 +6781,15 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.14.0.tgz",
|
||||
"integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.15.0.tgz",
|
||||
"integrity": "sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.14.0",
|
||||
"@prisma/engines": "6.14.0"
|
||||
"@prisma/config": "6.15.0",
|
||||
"@prisma/engines": "6.15.0"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||
"@preline/dropdown": "^3.0.1",
|
||||
"@preline/tooltip": "^3.0.0",
|
||||
"@prisma/client": "^6.14.0",
|
||||
"@prisma/client": "^6.15.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"csgo-sharecode": "^3.1.2",
|
||||
@ -65,7 +65,7 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.0",
|
||||
"prisma": "^6.14.0",
|
||||
"prisma": "^6.15.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.19.4",
|
||||
|
||||
@ -323,6 +323,8 @@ model MapVote {
|
||||
currentIdx Int @default(0)
|
||||
locked Boolean @default(false)
|
||||
opensAt DateTime?
|
||||
|
||||
leadMinutes Int @default(60)
|
||||
|
||||
adminEditingBy String?
|
||||
adminEditingSince DateTime?
|
||||
|
||||
@ -66,7 +66,7 @@ function buildSteps(bestOf: number, teamAId: string, teamBId: string) {
|
||||
] as const
|
||||
}
|
||||
|
||||
function shapeState(vote: any) {
|
||||
function shapeState(vote: any, match?: any) {
|
||||
const steps = [...vote.steps]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((s: any) => ({
|
||||
@ -78,20 +78,24 @@ function shapeState(vote: any) {
|
||||
chosenBy: s.chosenBy ?? null,
|
||||
}))
|
||||
|
||||
const opensAtDate = vote.opensAt ? new Date(vote.opensAt) : null
|
||||
const baseDate = match?.matchDate ?? match?.demoDate ?? null
|
||||
const leadMinutes =
|
||||
opensAtDate && baseDate
|
||||
? Math.max(0, Math.round((new Date(baseDate).getTime() - opensAtDate.getTime()) / 60000))
|
||||
: null
|
||||
|
||||
return {
|
||||
bestOf : vote.bestOf,
|
||||
mapPool : vote.mapPool as string[],
|
||||
currentIndex: vote.currentIdx,
|
||||
locked : vote.locked as boolean,
|
||||
opensAt : vote.opensAt ? new Date(vote.opensAt).toISOString() : null,
|
||||
opensAt : opensAtDate ? opensAtDate.toISOString() : null,
|
||||
leadMinutes,
|
||||
steps,
|
||||
// Admin-Edit Shape
|
||||
adminEdit: vote.adminEditingBy
|
||||
? {
|
||||
enabled: true,
|
||||
by: vote.adminEditingBy as string,
|
||||
since: vote.adminEditingSince ? new Date(vote.adminEditingSince).toISOString() : null,
|
||||
}
|
||||
? { enabled: true, by: vote.adminEditingBy as string,
|
||||
since: vote.adminEditingSince ? new Date(vote.adminEditingSince).toISOString() : null }
|
||||
: { enabled: false, by: null, since: null },
|
||||
}
|
||||
}
|
||||
@ -418,7 +422,7 @@ function deriveChosenSteps(vote: any) {
|
||||
export async function GET(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||
try {
|
||||
const matchId = params.matchId
|
||||
if (!matchId) return NextResponse.json({ message: 'Missing id' }, { status: 400 })
|
||||
if (!matchId) return NextResponse.json({ message: 'Missing matchId' }, { status: 400 })
|
||||
|
||||
const { match, vote } = await ensureVote(matchId)
|
||||
if (!match || !vote) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 })
|
||||
@ -427,7 +431,7 @@ export async function GET(req: NextRequest, { params }: { params: { matchId: str
|
||||
const mapVisuals = buildMapVisuals(match.id, vote.mapPool)
|
||||
|
||||
return NextResponse.json(
|
||||
{ ...shapeState(vote), mapVisuals, teams },
|
||||
{ ...shapeState(vote, match), mapVisuals, teams }, // 👈 match mitgeben
|
||||
{ headers: { 'Cache-Control': 'no-store' } },
|
||||
)
|
||||
} catch (e) {
|
||||
@ -461,13 +465,16 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
||||
}
|
||||
|
||||
const updated = await setAdminEdit(vote.id, body.adminEdit ? me.steamId : null)
|
||||
|
||||
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: 'map-vote-updated',
|
||||
payload: { matchId, opensAt: updated.opensAt ?? null },
|
||||
})
|
||||
|
||||
const teams = buildTeamsPayloadFromMatch(match)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated.mapPool)
|
||||
|
||||
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
||||
}
|
||||
|
||||
/* -------- Wenn anderer Admin editiert: Voting sperren -------- */
|
||||
@ -492,7 +499,10 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
||||
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 })
|
||||
await sendServerSSEMessage({
|
||||
type: 'map-vote-updated',
|
||||
payload: { matchId, opensAt: updated?.opensAt ?? null },
|
||||
})
|
||||
|
||||
const teams = buildTeamsPayloadFromMatch(match)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||
@ -519,7 +529,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
||||
await exportMatchToSftpDirect(match, updated)
|
||||
}
|
||||
|
||||
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
||||
}
|
||||
|
||||
const available = computeAvailableMaps(vote.mapPool, stepsSorted)
|
||||
@ -546,7 +556,10 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
||||
include: { steps: true },
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||
await sendServerSSEMessage({
|
||||
type: 'map-vote-updated',
|
||||
payload: { matchId, opensAt: updated?.opensAt ?? null },
|
||||
})
|
||||
|
||||
const teams = buildTeamsPayloadFromMatch(match)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||
@ -573,7 +586,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
||||
await exportMatchToSftpDirect(match, updated)
|
||||
}
|
||||
|
||||
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
||||
}
|
||||
|
||||
// Rechte prüfen (Admin oder Leader des Teams am Zug)
|
||||
@ -639,7 +652,10 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
||||
include: { steps: true },
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||
await sendServerSSEMessage({
|
||||
type: 'map-vote-updated',
|
||||
payload: { matchId, opensAt: updated?.opensAt ?? null },
|
||||
})
|
||||
|
||||
const teams = buildTeamsPayloadFromMatch(match)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||
@ -665,7 +681,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
||||
await exportMatchToSftpDirect(match, updated)
|
||||
}
|
||||
|
||||
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
||||
} catch (e) {
|
||||
console.error('[map-vote][POST] error', e)
|
||||
return NextResponse.json({ message: 'Aktion fehlgeschlagen' }, { status: 500 })
|
||||
|
||||
@ -1,18 +1,76 @@
|
||||
// /app/api/matches/[id]/meta/route.ts
|
||||
// /app/api/matches/[matchId]/meta/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/app/lib/auth'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export async function PUT(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||
const id = params.matchId
|
||||
export const runtime = 'nodejs' // 👈 wie bei mapvote
|
||||
export const dynamic = 'force-dynamic' // 👈 wie bei mapvote
|
||||
|
||||
// Hilfsfunktion: akzeptiert Date | string | number | null | undefined
|
||||
function parseDateOrNull(v: unknown): Date | null | undefined {
|
||||
if (typeof v === 'undefined') return undefined
|
||||
if (v === null) return null
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
const ms = v >= 1e12 ? v : v * 1000
|
||||
const d = new Date(ms)
|
||||
return Number.isNaN(d.getTime()) ? undefined : d
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
const s = v.trim()
|
||||
if (s === '') return null
|
||||
const hasTZ = /[zZ]|[+\-]\d{2}:\d{2}$/.test(s)
|
||||
if (!hasTZ) {
|
||||
const mDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s)
|
||||
if (mDate) {
|
||||
const d = new Date(+mDate[1], +mDate[2] - 1, +mDate[3], 0, 0, 0, 0)
|
||||
return Number.isNaN(d.getTime()) ? undefined : d
|
||||
}
|
||||
const mDateTime = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(s)
|
||||
if (mDateTime) {
|
||||
const d = new Date(
|
||||
+mDateTime[1], +mDateTime[2] - 1, +mDateTime[3],
|
||||
+mDateTime[4], +mDateTime[5], mDateTime[6] ? +mDateTime[6] : 0, 0
|
||||
)
|
||||
return Number.isNaN(d.getTime()) ? undefined : d
|
||||
}
|
||||
}
|
||||
const t = Date.parse(s)
|
||||
if (!Number.isNaN(t)) return new Date(t)
|
||||
return undefined
|
||||
}
|
||||
if (v instanceof Date && !Number.isNaN(v.getTime())) return v
|
||||
return undefined
|
||||
}
|
||||
|
||||
// wie in mapvote: Basiszeit -> opensAt
|
||||
function voteOpensAt(base: Date, leadMinutes: number) {
|
||||
return new Date(base.getTime() - leadMinutes * 60_000)
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { matchId: string } }
|
||||
) {
|
||||
const id = params?.matchId
|
||||
if (!id) return NextResponse.json({ error: 'Missing matchId in route params' }, { status: 400 })
|
||||
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const me = session?.user
|
||||
if (!me?.steamId) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const { title, matchType, teamAId, teamBId, matchDate, map, voteLeadMinutes } = body ?? {}
|
||||
const {
|
||||
title,
|
||||
matchType,
|
||||
teamAId,
|
||||
teamBId,
|
||||
matchDate,
|
||||
map,
|
||||
voteLeadMinutes, // optional
|
||||
demoDate,
|
||||
} = body ?? {}
|
||||
|
||||
try {
|
||||
const match = await prisma.match.findUnique({
|
||||
@ -32,24 +90,38 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
// 1) Matching-Daten zusammenbauen
|
||||
const updateData: any = {}
|
||||
if (typeof title !== 'undefined') updateData.title = title
|
||||
if (typeof matchType === 'string') updateData.matchType = matchType
|
||||
if (typeof map !== 'undefined') updateData.map = map
|
||||
if (typeof teamAId !== 'undefined') updateData.teamAId = teamAId
|
||||
if (typeof teamBId !== 'undefined') updateData.teamBId = teamBId
|
||||
if (typeof matchDate === 'string' || matchDate === null) {
|
||||
updateData.matchDate = matchDate ? new Date(matchDate) : null
|
||||
|
||||
const parsedMatchDate = parseDateOrNull(matchDate)
|
||||
if (parsedMatchDate !== undefined) updateData.matchDate = parsedMatchDate
|
||||
|
||||
const parsedDemoDate = parseDateOrNull(demoDate)
|
||||
if (parsedDemoDate !== undefined) {
|
||||
updateData.demoDate = parsedDemoDate
|
||||
} else if (parsedMatchDate instanceof Date) {
|
||||
// demoDate mitziehen, wenn matchDate geändert und demoDate nicht gesendet wurde
|
||||
updateData.demoDate = parsedMatchDate
|
||||
}
|
||||
|
||||
const lead = Number.isFinite(Number(voteLeadMinutes)) ? Number(voteLeadMinutes) : 60
|
||||
let opensAt: Date | null = null
|
||||
if (updateData.matchDate instanceof Date) {
|
||||
opensAt = new Date(updateData.matchDate.getTime() - lead * 60 * 1000)
|
||||
} else if (match.matchDate) {
|
||||
opensAt = new Date(match.matchDate.getTime() - lead * 60 * 1000)
|
||||
}
|
||||
// 2) Lead bestimmen (Body > gespeicherter Wert > default 60)
|
||||
const leadBodyRaw = Number(voteLeadMinutes)
|
||||
const leadBody = Number.isFinite(leadBodyRaw) ? leadBodyRaw : undefined
|
||||
const currentLead = match.mapVote?.leadMinutes ?? 60 // erfordert Feld im Schema
|
||||
const leadMinutes = leadBody ?? currentLead
|
||||
|
||||
// 3) Basiszeit (neu oder alt)
|
||||
const baseDate: Date | null =
|
||||
(updateData.matchDate instanceof Date ? updateData.matchDate : null) ??
|
||||
(match.matchDate ?? null) ??
|
||||
(match.demoDate ?? null)
|
||||
|
||||
// 4) Updaten & opensAt ggf. neu setzen – analog zu mapvote
|
||||
const updated = await prisma.$transaction(async (tx) => {
|
||||
const m = await tx.match.update({
|
||||
where: { id },
|
||||
@ -57,21 +129,34 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
||||
include: { mapVote: true },
|
||||
})
|
||||
|
||||
if (opensAt) {
|
||||
// Wenn wir eine Basiszeit haben → opensAt neu berechnen
|
||||
if (baseDate) {
|
||||
const opensAt = voteOpensAt(baseDate, leadMinutes)
|
||||
|
||||
if (!m.mapVote) {
|
||||
// MapVote existiert noch nicht → nur opensAt/leadMinutes anlegen (Schritte erstellt get /mapvote)
|
||||
await tx.mapVote.create({
|
||||
data: {
|
||||
matchId: m.id,
|
||||
leadMinutes: leadMinutes,
|
||||
opensAt,
|
||||
// entferne Felder, die es im Schema nicht gibt (z. B. isOpen)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await tx.mapVote.update({
|
||||
where: { id: m.mapVote.id },
|
||||
data: { opensAt },
|
||||
data: {
|
||||
...(leadBody !== undefined ? { leadMinutes: leadMinutes } : {}),
|
||||
opensAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if (leadBody !== undefined && m.mapVote) {
|
||||
// Keine Basiszeit-Änderung, aber Lead explizit gesetzt → nur leadMinutes persistieren
|
||||
await tx.mapVote.update({
|
||||
where: { id: m.mapVote.id },
|
||||
data: { leadMinutes: leadMinutes },
|
||||
})
|
||||
}
|
||||
|
||||
return tx.match.findUnique({
|
||||
@ -86,16 +171,32 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
||||
|
||||
if (!updated) return NextResponse.json({ error: 'Reload failed' }, { status: 500 })
|
||||
|
||||
// 5) Events senden – Shape identisch zur mapvote-Route
|
||||
// a) Für das Voting/Countdown: map-vote-updated (liefert opensAt)
|
||||
await sendServerSSEMessage({
|
||||
type: 'map-vote-updated',
|
||||
payload: {
|
||||
matchId: updated.id,
|
||||
opensAt: updated.mapVote?.opensAt ?? null, // JSON.stringify -> ISO
|
||||
},
|
||||
})
|
||||
|
||||
// b) Zusätzlich Meta-Event für andere UIs
|
||||
await sendServerSSEMessage({
|
||||
type: 'match-meta-updated',
|
||||
payload: {
|
||||
matchId: updated.id,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
|
||||
// (Optional) allgemeines match-updated, falls du andere Bereiche triggern willst
|
||||
await sendServerSSEMessage({
|
||||
type: 'match-updated',
|
||||
payload: { matchId: updated.id, updatedAt: new Date().toISOString() },
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: 'map-vote-updated',
|
||||
payload: { matchId: updated.id, opensAt: updated.mapVote?.opensAt ?? null },
|
||||
})
|
||||
|
||||
// 6) Response (no-store)
|
||||
return NextResponse.json({
|
||||
id: updated.id,
|
||||
title: updated.title,
|
||||
@ -103,6 +204,7 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
||||
teamAId: updated.teamAId,
|
||||
teamBId: updated.teamBId,
|
||||
matchDate: updated.matchDate,
|
||||
demoDate: updated.demoDate,
|
||||
map: updated.map,
|
||||
mapVote: updated.mapVote,
|
||||
}, { headers: { 'Cache-Control': 'no-store' } })
|
||||
@ -110,4 +212,4 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
||||
console.error(`PUT /matches/${id}/meta failed:`, err)
|
||||
return NextResponse.json({ error: 'Failed to update match meta' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,14 @@ export async function GET(_: Request, { params }: { params: { matchId: string }
|
||||
players: { include: { user: true, stats: true, team: true } },
|
||||
teamAUsers: { include: { team: true } },
|
||||
teamBUsers: { include: { team: true } },
|
||||
mapVote: {
|
||||
include: {
|
||||
steps: {
|
||||
orderBy: { order: 'asc' },
|
||||
select: { order: true, action: true, map: true, teamId: true, chosenAt: true, chosenBy: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!m) return NextResponse.json({ error: 'Match nicht gefunden' }, { status: 404 })
|
||||
@ -31,9 +39,24 @@ export async function GET(_: Request, { params }: { params: { matchId: string }
|
||||
const payload =
|
||||
m.matchType === 'community' && isFuture(m)
|
||||
? await buildCommunityFuturePayload(m)
|
||||
: buildDefaultPayload(m)
|
||||
: buildDefaultPayload(m);
|
||||
|
||||
return NextResponse.json(payload, { headers: { 'Cache-Control': 'no-store' } })
|
||||
// ⬇️ Zusatz: opensAt (und leadMinutes) an die Antwort hängen
|
||||
const baseTs = (m.matchDate ?? m.demoDate)?.getTime?.() ?? null;
|
||||
const opensAt = m.mapVote?.opensAt ?? null;
|
||||
const leadMinutes =
|
||||
opensAt && baseTs != null
|
||||
? Math.max(0, Math.round((baseTs - opensAt.getTime()) / 60000))
|
||||
: null;
|
||||
|
||||
return NextResponse.json({
|
||||
...payload,
|
||||
mapVote: {
|
||||
...(payload as any).mapVote,
|
||||
opensAt, // <- wichtig
|
||||
leadMinutes, // optional, aber nett zu haben
|
||||
},
|
||||
}, { headers: { 'Cache-Control': 'no-store' } });
|
||||
} catch (err) {
|
||||
console.error(`GET /matches/${params.matchId} failed:`, err)
|
||||
return NextResponse.json({ error: 'Failed to load match' }, { status: 500 })
|
||||
@ -184,6 +207,14 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
||||
players: { include: { user: true, stats: true, team: true } },
|
||||
teamAUsers: { include: { team: true } },
|
||||
teamBUsers: { include: { team: true } },
|
||||
mapVote: {
|
||||
include: {
|
||||
steps: {
|
||||
orderBy: { order: 'asc' },
|
||||
select: { order: true, action: true, map: true, teamId: true, chosenAt: true, chosenBy: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!updated) return NextResponse.json({ error: 'Match konnte nach Update nicht geladen werden' }, { status: 500 })
|
||||
|
||||
@ -5,7 +5,7 @@ import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { format } from 'date-fns'
|
||||
import { format } from 'date-fns' // 👈 neu
|
||||
import { de } from 'date-fns/locale'
|
||||
import Switch from '@/app/components/Switch'
|
||||
import Button from './Button'
|
||||
@ -41,6 +41,20 @@ function getNextHourDefaults() {
|
||||
return { dateStr, timeStr }
|
||||
}
|
||||
|
||||
// 👇 Helper für Map-Vote-Status
|
||||
function getMapVoteState(m: Match, nowMs: number) {
|
||||
const opensAt = m?.mapVote?.opensAt ? new Date(m.mapVote.opensAt) : null
|
||||
const locked = m?.mapVote?.locked ?? false
|
||||
if (!opensAt) return { hasVote: false as const }
|
||||
|
||||
const opensAtMs = opensAt.getTime()
|
||||
const isOpen = !locked && opensAtMs <= nowMs
|
||||
const opensInMs = Math.max(0, opensAtMs - nowMs) // nie negativ
|
||||
|
||||
return { hasVote: true as const, isOpen, opensAt, opensInMs }
|
||||
}
|
||||
|
||||
|
||||
export default function CommunityMatchList({ matchType }: Props) {
|
||||
const { data: session } = useSession()
|
||||
const router = useRouter()
|
||||
@ -71,6 +85,13 @@ export default function CommunityMatchList({ matchType }: Props) {
|
||||
[teams]
|
||||
)
|
||||
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 1000)
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
// force re-render, damit isOpen (Vergleich mit Date.now) neu bewertet wird
|
||||
@ -252,52 +273,85 @@ export default function CommunityMatchList({ matchType }: Props) {
|
||||
const unfinished = !m.winnerTeam && m.scoreA == null && m.scoreB == null
|
||||
const isLive = started && unfinished
|
||||
const isOwnTeam = !!session?.user?.team &&
|
||||
(m.teamA.id === session.user.team || m.teamB.id === session.user.team)
|
||||
(m.teamA.id === session.user.team || m.teamB.id === session.user.team)
|
||||
const dimmed = onlyOwn && !isOwnTeam
|
||||
|
||||
// 👇 Map-Vote Status berechnen
|
||||
const mv = getMapVoteState(m, now)
|
||||
const opensText =
|
||||
mv.hasVote && !mv.isOpen && mv.opensAt
|
||||
? `öffnet in ${formatCountdown(mv.opensInMs)}`
|
||||
: null
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={m.id}
|
||||
href={`/match-details/${m.id}`}
|
||||
className={`
|
||||
relative flex flex-col items-center gap-4 bg-neutral-300 dark:bg-neutral-800
|
||||
text-gray-800 dark:text-white rounded-sm py-4
|
||||
hover:scale-105 hover:bg-neutral-400 hover:dark:bg-neutral-700
|
||||
hover:shadow-md h-[172px]
|
||||
grid grid-rows-[auto_1fr_auto] justify-items-center gap-3
|
||||
bg-neutral-300 dark:bg-neutral-800 text-gray-800 dark:text-white
|
||||
rounded-sm p-4 min-h-[210px]
|
||||
hover:scale-105 hover:bg-neutral-400 hover:dark:bg-neutral-700 hover:shadow-md
|
||||
transition-transform transition-opacity duration-300 ease-in-out
|
||||
${dimmed ? 'opacity-40' : 'opacity-100'}
|
||||
`}
|
||||
>
|
||||
{/* Live / Map-Vote Badge */}
|
||||
{isLive ? (
|
||||
<span className="absolute top-2 px-2 py-0.5 text-xs font-semibold rounded-full bg-red-500 text-white shadow">
|
||||
LIVE
|
||||
</span>
|
||||
) : (m.mapVote?.isOpen ? (
|
||||
<span className="absolute top-2 px-2 py-0.5 text-[11px] font-semibold rounded-full bg-green-600 text-white shadow">
|
||||
Map-Vote offen
|
||||
</span>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
{/* Zeile 1: Badges (immer Platz, auch wenn leer) */}
|
||||
<div className="flex flex-col items-center gap-1 min-h-6">
|
||||
{isLive ? (
|
||||
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-red-500 text-white shadow">
|
||||
LIVE
|
||||
</span>
|
||||
) : mv.hasVote && (mv.isOpen || opensText) ? (
|
||||
<>
|
||||
{mv.isOpen && (
|
||||
<span className="px-2 py-0.5 text-[11px] font-semibold rounded-full bg-green-600 text-white shadow">
|
||||
Map-Vote offen
|
||||
</span>
|
||||
)}
|
||||
{!mv.isOpen && opensText && (
|
||||
<span
|
||||
title={mv.opensAt?.toLocaleString('de-DE') ?? undefined}
|
||||
className="px-2 py-0.5 text-[11px] font-medium rounded-full bg-yellow-300 text-gray-900 dark:bg-yellow-500 dark:text-black shadow"
|
||||
>
|
||||
Map-Vote {opensText}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Zeile 2: Teams */}
|
||||
<div className="flex w-full justify-around items-center">
|
||||
<div className="flex flex-col items-center w-1/3">
|
||||
<Image src={getTeamLogo(m.teamA.logo)} alt={m.teamA.name} width={48} height={48} className="rounded-full border bg-white" />
|
||||
<span className="mt-2 text-xs">{m.teamA.name}</span>
|
||||
<Image
|
||||
src={getTeamLogo(m.teamA.logo)}
|
||||
alt={m.teamA.name}
|
||||
width={56}
|
||||
height={56}
|
||||
className="rounded-full border bg-white"
|
||||
/>
|
||||
<span className="mt-2 text-xs text-center line-clamp-1">{m.teamA.name}</span>
|
||||
</div>
|
||||
<span className="font-bold">vs</span>
|
||||
<div className="flex flex-col items-center w-1/3">
|
||||
<Image src={getTeamLogo(m.teamB.logo)} alt={m.teamB.name} width={48} height={48} className="rounded-full border bg-white" />
|
||||
<span className="mt-2 text-xs">{m.teamB.name}</span>
|
||||
<Image
|
||||
src={getTeamLogo(m.teamB.logo)}
|
||||
alt={m.teamB.name}
|
||||
width={56}
|
||||
height={56}
|
||||
className="rounded-full border bg-white"
|
||||
/>
|
||||
<span className="mt-2 text-xs text-center line-clamp-1">{m.teamB.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Datum + Uhrzeit: höher & highlight */}
|
||||
{/* Zeile 3: Datum & Uhrzeit */}
|
||||
<div className="flex flex-col items-center -mt-1 space-y-1">
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-[13px] font-bold shadow ring-1 ring-black/10
|
||||
${isLive ? 'bg-red-500 text-white' : 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-black'}
|
||||
`}
|
||||
${isLive ? 'bg-red-500 text-white' : 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-black'}
|
||||
`}
|
||||
>
|
||||
{format(new Date(m.demoDate), 'dd.MM.yyyy', { locale: de })}
|
||||
</span>
|
||||
@ -441,3 +495,13 @@ export default function CommunityMatchList({ matchType }: Props) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatCountdown(ms: number) {
|
||||
if (ms <= 0) return '0:00:00'
|
||||
const totalSec = Math.floor(ms / 1000)
|
||||
const h = Math.floor(totalSec / 3600)
|
||||
const m = Math.floor((totalSec % 3600) / 60)
|
||||
const s = totalSec % 60
|
||||
const pad = (n:number)=>String(n).padStart(2,'0')
|
||||
return `${h}:${pad(m)}:${pad(s)}`
|
||||
}
|
||||
|
||||
@ -157,8 +157,10 @@ export default function EditMatchMetaModal({
|
||||
}
|
||||
|
||||
setSaved(true)
|
||||
onSaved?.()
|
||||
onClose()
|
||||
setTimeout(() => {
|
||||
onSaved?.()
|
||||
}, 0)
|
||||
} catch (e: any) {
|
||||
console.error('[EditMatchMetaModal] save error:', e)
|
||||
setError(e?.message || 'Speichern fehlgeschlagen')
|
||||
@ -167,8 +169,6 @@ export default function EditMatchMetaModal({
|
||||
}
|
||||
}
|
||||
|
||||
if (!show) return null
|
||||
|
||||
// Platzhalter mit aktuellem Namen (falls Options noch laden)
|
||||
const teamAPlaceholder = loadingTeams ? 'Teams laden …' : (defaultTeamAName || 'Team A wählen …')
|
||||
const teamBPlaceholder = loadingTeams ? 'Teams laden …' : (defaultTeamBName || 'Team B wählen …')
|
||||
@ -177,7 +177,7 @@ export default function EditMatchMetaModal({
|
||||
<Modal
|
||||
id="edit-match-meta"
|
||||
title="Matchdaten bearbeiten"
|
||||
show
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
onSave={handleSave}
|
||||
closeButtonTitle={saved ? '✓ Gespeichert' : saving ? 'Speichern …' : 'Speichern'}
|
||||
|
||||
@ -149,10 +149,7 @@ export default function EditMatchPlayersModal (props: Props) {
|
||||
try {
|
||||
const body = {
|
||||
players: [
|
||||
/* akt. Auswahl für die bearbeitete Seite */
|
||||
...selected.map(steamId => ({ steamId, teamId: team.id })),
|
||||
|
||||
/* unveränderte Gegenseite unbedingt mitschicken! */
|
||||
...otherInit.map(steamId => ({ steamId, teamId: other.id })),
|
||||
],
|
||||
}
|
||||
@ -165,7 +162,13 @@ export default function EditMatchPlayersModal (props: Props) {
|
||||
if (!res.ok) throw new Error()
|
||||
|
||||
setSaved(true)
|
||||
onSaved?.()
|
||||
|
||||
// ⏳ 3 Sekunden warten, dann schließen und danach refreshen
|
||||
setTimeout(() => {
|
||||
onClose?.()
|
||||
// onSaved (z. B. router.refresh) im nächsten Tick nach dem Schließen
|
||||
setTimeout(() => { onSaved?.() }, 0)
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
console.error('[EditMatchPlayersModal] save error:', e)
|
||||
} finally {
|
||||
@ -173,18 +176,18 @@ export default function EditMatchPlayersModal (props: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ---- Listen trennen ------------------------------------- */
|
||||
const active = players.filter(p => selected.includes(p.steamId))
|
||||
const inactive = players.filter(p => !selected.includes(p.steamId))
|
||||
|
||||
/* ---- UI -------------------------------------------------- */
|
||||
if (!show) return null
|
||||
|
||||
return (
|
||||
<Modal
|
||||
id="edit-match-players"
|
||||
title={`Spieler bearbeiten – ${team.name ?? 'Team'}`}
|
||||
show
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
onSave={handleSave}
|
||||
closeButtonTitle={
|
||||
|
||||
@ -1,24 +1,52 @@
|
||||
// MapVoteBanner.tsx
|
||||
// /app/components/MapVoteBanner.tsx
|
||||
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useSSEStore } from '@/app/lib/useSSEStore'
|
||||
import type { MapVoteState } from '../types/mapvote'
|
||||
import { MATCH_EVENTS } from '@/app/lib/sseEvents'
|
||||
|
||||
type Props = { match: any; initialNow: number }
|
||||
type Props = {
|
||||
match: any
|
||||
initialNow: number
|
||||
matchBaseTs: number | null
|
||||
sseOpensAtTs?: number | null
|
||||
sseLeadMinutes?: number | null
|
||||
}
|
||||
|
||||
export default function MapVoteBanner({ match, initialNow }: Props) {
|
||||
function formatCountdown(ms: number) {
|
||||
if (ms <= 0) return '0:00:00'
|
||||
const totalSec = Math.floor(ms / 1000)
|
||||
const h = Math.floor(totalSec / 3600)
|
||||
const m = Math.floor((totalSec % 3600) / 60)
|
||||
const s = totalSec % 60
|
||||
const pad = (n:number)=>String(n).padStart(2,'0')
|
||||
return `${h}:${pad(m)}:${pad(s)}`
|
||||
}
|
||||
|
||||
function formatLead(minutes: number) {
|
||||
if (!Number.isFinite(minutes) || minutes <= 0) return 'zum Matchbeginn'
|
||||
const h = Math.floor(minutes / 60)
|
||||
const m = minutes % 60
|
||||
if (h > 0 && m > 0) return `${h}h ${m}min`
|
||||
if (h > 0) return `${h}h`
|
||||
return `${m}min`
|
||||
}
|
||||
|
||||
|
||||
export default function MapVoteBanner({ match, initialNow, matchBaseTs, sseOpensAtTs, sseLeadMinutes }: Props) {
|
||||
const router = useRouter()
|
||||
const { data: session } = useSession()
|
||||
const { lastEvent } = useSSEStore()
|
||||
|
||||
// ✅ eine Uhr, deterministisch bei Hydration (kommt als Prop vom Server)
|
||||
const [now, setNow] = useState(initialNow)
|
||||
|
||||
const [state, setState] = useState<MapVoteState | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [leadOverride, setLeadOverride] = useState<number | null>(null);
|
||||
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null)
|
||||
|
||||
// deterministische Hydration + 1s-Ticker
|
||||
const [now, setNow] = useState(initialNow)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
@ -36,33 +64,90 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
|
||||
setError(e?.message ?? 'Unbekannter Fehler')
|
||||
}
|
||||
}, [match.id])
|
||||
|
||||
const matchDateTs = useMemo(
|
||||
() => (typeof matchBaseTs === 'number' ? matchBaseTs : null),
|
||||
[matchBaseTs]
|
||||
)
|
||||
|
||||
// ✅ tickt NUR im Client, nach Hydration
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 1000)
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
// initial
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
// Live-Refresh via SSE
|
||||
// 🔁 Neu laden, wenn Match-Metadaten (z. B. matchDate/bestOf) sich durch refresh ändern
|
||||
useEffect(() => { load() }, [match.matchDate, match.demoDate, match.bestOf, load])
|
||||
|
||||
// 🔁 Live-Refresh via SSE
|
||||
useEffect(() => {
|
||||
if (!lastEvent) return
|
||||
// ⬇️ reagiert auf alle match-bezogenen Events: vote-updated, admin-edit, reset, ready, lineup, ...
|
||||
if (!MATCH_EVENTS.has(lastEvent.type)) return
|
||||
const { type } = lastEvent as any
|
||||
const evt = (lastEvent as any).payload ?? lastEvent
|
||||
if (evt?.matchId !== match.id) return
|
||||
|
||||
const matchId = lastEvent.payload?.matchId
|
||||
if (matchId !== match.id) return
|
||||
const RELOAD_TYPES = new Set([
|
||||
'map-vote-updated','map-vote-reset','map-vote-locked','map-vote-unlocked',
|
||||
'match-updated','match-lineup-updated',
|
||||
])
|
||||
if (!RELOAD_TYPES.has(type)) return
|
||||
|
||||
load()
|
||||
}, [lastEvent, match.id, load])
|
||||
const rawLead = evt?.leadMinutes
|
||||
const parsedLead = (rawLead !== undefined && rawLead !== null) ? Number(rawLead) : undefined
|
||||
const nextOpensAtISO =
|
||||
evt?.opensAt
|
||||
? (typeof evt.opensAt === 'string' ? evt.opensAt : new Date(evt.opensAt).toISOString())
|
||||
: undefined
|
||||
|
||||
// Öffnet 1h vor Match-/Demotermin (stabil, ohne Date.now() im Render)
|
||||
// sofortige lokale Overrides, ohne auf fetch zu warten
|
||||
if (nextOpensAtISO) {
|
||||
setOpensAtOverride(new Date(nextOpensAtISO).getTime())
|
||||
} else if (Number.isFinite(parsedLead) && matchDateTs != null) {
|
||||
setOpensAtOverride(matchDateTs - (parsedLead as number) * 60_000)
|
||||
}
|
||||
if (Number.isFinite(parsedLead)) setLeadOverride(parsedLead as number)
|
||||
|
||||
// sichtbares Mergen (für UI-Text)
|
||||
if (nextOpensAtISO !== undefined || Number.isFinite(parsedLead)) {
|
||||
setState(prev => ({
|
||||
...(prev ?? {} as any),
|
||||
...(nextOpensAtISO !== undefined ? { opensAt: nextOpensAtISO } : {}),
|
||||
...(Number.isFinite(parsedLead) ? { leadMinutes: parsedLead } : {}),
|
||||
}) as any)
|
||||
} else {
|
||||
// nur nachladen, wenn Event keine konkreten Werte trug
|
||||
load()
|
||||
}
|
||||
}, [lastEvent, match.id, matchDateTs, load])
|
||||
|
||||
// Öffnet wann?
|
||||
const opensAt = useMemo(() => {
|
||||
// höchste Priorität: vom Parent (MatchDetails) gereichter TS
|
||||
if (typeof sseOpensAtTs === 'number') return sseOpensAtTs
|
||||
// dann lokaler SSE-Override
|
||||
if (opensAtOverride != null) return opensAtOverride
|
||||
// dann Serverwert aus /mapvote
|
||||
if (state?.opensAt) return new Date(state.opensAt).getTime()
|
||||
const base = new Date(match.matchDate ?? match.demoDate ?? initialNow)
|
||||
return base.getTime() - 60 * 60 * 1000
|
||||
}, [state?.opensAt, match.matchDate, match.demoDate, initialNow])
|
||||
// Fallback aus Basis + Lead
|
||||
if (matchDateTs == null) return new Date(initialNow).getTime()
|
||||
const lead = (typeof sseLeadMinutes === 'number')
|
||||
? sseLeadMinutes
|
||||
: (leadOverride ?? (Number.isFinite(state?.leadMinutes) ? state!.leadMinutes as number : 60))
|
||||
return matchDateTs - lead * 60_000
|
||||
}, [sseOpensAtTs, opensAtOverride, state?.opensAt, matchDateTs, initialNow, sseLeadMinutes, leadOverride, state?.leadMinutes])
|
||||
|
||||
// Text „startet X vor Matchbeginn“
|
||||
const leadMinutes = useMemo(() => {
|
||||
if (matchDateTs != null && opensAt != null) {
|
||||
return Math.max(0, Math.round((matchDateTs - opensAt) / 60_000))
|
||||
}
|
||||
if (typeof sseLeadMinutes === 'number') return sseLeadMinutes
|
||||
if (leadOverride != null) return leadOverride
|
||||
if (Number.isFinite(state?.leadMinutes)) return state!.leadMinutes as number
|
||||
return 60
|
||||
}, [matchDateTs, opensAt, sseLeadMinutes, leadOverride, state?.leadMinutes])
|
||||
|
||||
const isOpen = now >= opensAt
|
||||
const msToOpen = Math.max(opensAt - now, 0)
|
||||
@ -72,7 +157,6 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
|
||||
? (current.teamId === match.teamA?.id ? match.teamA?.name : match.teamB?.name)
|
||||
: null
|
||||
|
||||
// ⚠️ leader ist bei dir ein Player-Objekt → .steamId vergleichen
|
||||
const isLeaderA = !!session?.user?.steamId && match.teamA?.leader?.steamId === session.user.steamId
|
||||
const isLeaderB = !!session?.user?.steamId && match.teamB?.leader?.steamId === session.user.steamId
|
||||
const isAdmin = !!session?.user?.isAdmin
|
||||
@ -128,7 +212,7 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
|
||||
? ' • Auswahl fixiert'
|
||||
: isOpen
|
||||
? (whoIsUp ? ` • am Zug: ${whoIsUp}` : ' • läuft')
|
||||
: ' • startet 1h vor Matchbeginn'}
|
||||
: ` • startet ${formatLead(leadMinutes)} vor Matchbeginn`}
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mt-0.5">
|
||||
@ -219,14 +303,4 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatCountdown(ms: number) {
|
||||
if (ms <= 0) return '0:00:00'
|
||||
const totalSec = Math.floor(ms / 1000)
|
||||
const h = Math.floor(totalSec / 3600)
|
||||
const m = Math.floor((totalSec % 3600) / 60)
|
||||
const s = totalSec % 60
|
||||
const pad = (n:number)=>String(n).padStart(2,'0')
|
||||
return `${h}:${pad(m)}:${pad(s)}`
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
// /app/components/MapVotePanel.tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
|
||||
@ -35,6 +36,20 @@ const fmtCountdown = (ms: number) => {
|
||||
return `${h}:${pad(m)}:${pad(s)}`
|
||||
}
|
||||
|
||||
const toMs = (v: unknown): number | null => {
|
||||
if (v == null) return null
|
||||
if (typeof v === 'number' && Number.isFinite(v)) return v >= 1e12 ? v : v * 1000
|
||||
if (typeof v === 'string') {
|
||||
const t = Date.parse(v)
|
||||
return Number.isNaN(t) ? null : t
|
||||
}
|
||||
if (v instanceof Date) {
|
||||
const t = v.getTime()
|
||||
return Number.isNaN(t) ? null : t
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/* =================== Component =================== */
|
||||
|
||||
export default function MapVotePanel({ match }: Props) {
|
||||
@ -50,18 +65,13 @@ export default function MapVotePanel({ match }: Props) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [adminEditMode, setAdminEditMode] = useState(false)
|
||||
const [overlayShownOnce, setOverlayShownOnce] = useState(false)
|
||||
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null);
|
||||
const [leadOverride, setLeadOverride] = useState<number | null>(null);
|
||||
|
||||
/* -------- Timers / open window -------- */
|
||||
const opensAtTs = useMemo(() => {
|
||||
const base = new Date(match.matchDate ?? match.demoDate ?? Date.now())
|
||||
return base.getTime() - 60 * 60 * 1000
|
||||
}, [match.matchDate, match.demoDate])
|
||||
|
||||
const [nowTs, setNowTs] = useState(() => Date.now())
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setNowTs(Date.now()), 1000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
const matchBaseTs = useMemo(() => {
|
||||
const raw = match.matchDate ?? match.demoDate ?? Date.now();
|
||||
return new Date(raw).getTime();
|
||||
}, [match.matchDate, match.demoDate]);
|
||||
|
||||
/* -------- Overlay integration -------- */
|
||||
const overlayIsForThisMatch = overlayData?.matchId === match.id
|
||||
@ -107,14 +117,115 @@ export default function MapVotePanel({ match }: Props) {
|
||||
}
|
||||
}, [match.id])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
useEffect(() => { load() }, [load, match.matchDate, match.demoDate])
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastEvent) return
|
||||
if (!MATCH_EVENTS.has(lastEvent.type)) return
|
||||
if (lastEvent.payload?.matchId !== match.id) return
|
||||
load()
|
||||
}, [lastEvent, match.id, load])
|
||||
setOpensAtOverride(null)
|
||||
}, [match.matchDate, match.demoDate])
|
||||
|
||||
// 🔔 SSE: wie in MatchDetails — opensAt/leadMinutes direkt aus dem Event übernehmen
|
||||
useEffect(() => {
|
||||
console.log("lastEvent: ", lastEvent);
|
||||
if (!lastEvent) return;
|
||||
|
||||
const { type, payload } = lastEvent as any;
|
||||
const evt = payload ?? lastEvent;
|
||||
const evtMatchId = evt?.matchId ?? (lastEvent as any)?.matchId;
|
||||
if (evtMatchId !== match.id) return;
|
||||
|
||||
if (type === 'map-vote-updated' || type === 'match-meta-updated') {
|
||||
// 1) opensAt aus Event direkt übernehmen (ISO → ms)
|
||||
if (evt?.opensAt) {
|
||||
const ts =
|
||||
typeof evt.opensAt === 'string'
|
||||
? new Date(evt.opensAt).getTime()
|
||||
: new Date(evt.opensAt).getTime();
|
||||
if (Number.isFinite(ts)) setOpensAtOverride(ts);
|
||||
}
|
||||
|
||||
// 2) leadMinutes mitschneiden und ggf. opensAt daraus ableiten,
|
||||
// falls im Event kein opensAt enthalten war
|
||||
if (Number.isFinite(evt?.leadMinutes)) {
|
||||
const lead = Number(evt.leadMinutes);
|
||||
setLeadOverride(lead);
|
||||
|
||||
if (!evt?.opensAt) {
|
||||
const base = new Date(
|
||||
match.matchDate ?? match.demoDate ?? Date.now()
|
||||
).getTime();
|
||||
setOpensAtOverride(base - lead * 60_000);
|
||||
}
|
||||
}
|
||||
|
||||
// WICHTIG: Kein load()/refresh hier – genau wie in MatchDetails
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh bei Events, die Meta/Lineup betreffen – wie in MatchDetails
|
||||
const REFRESH_TYPES = new Set([
|
||||
'map-vote-reset',
|
||||
'map-vote-locked',
|
||||
'map-vote-unlocked',
|
||||
'match-updated',
|
||||
'match-lineup-updated',
|
||||
'match-meta-updated',
|
||||
]);
|
||||
if (REFRESH_TYPES.has(type)) {
|
||||
// analog zu MatchDetails: UI sanft aktualisieren
|
||||
router.refresh?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: bekannte Match-Events → Daten nachladen
|
||||
if (MATCH_EVENTS.has(type)) load();
|
||||
}, [lastEvent, match.id, match.matchDate, match.demoDate, router, load]);
|
||||
|
||||
|
||||
// 📅 Öffnungszeit robust ableiten (Server -> SSE-Override -> Lead-Fallback)
|
||||
const openTs = useMemo(() => {
|
||||
if (opensAtOverride != null) return opensAtOverride
|
||||
const srv = toMs(state?.opensAt)
|
||||
if (srv != null) return srv
|
||||
const lead = leadOverride ?? (Number.isFinite(state?.leadMinutes) ? (state!.leadMinutes as number) : 60)
|
||||
return matchBaseTs - lead * 60_000
|
||||
}, [opensAtOverride, state?.opensAt, state?.leadMinutes, leadOverride, matchBaseTs])
|
||||
|
||||
// --- Hydration-Schutz ---
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => { setMounted(true) }, [])
|
||||
|
||||
// --- Countdown-State: NICHT mit Date.now() initialisieren ---
|
||||
const [msLeft, setMsLeft] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!Number.isFinite(openTs)) console.warn('[MapVotePanel] openTs invalid:', openTs, { state, opensAtOverride, leadOverride, matchBaseTs })
|
||||
}, [openTs, state, opensAtOverride, leadOverride, matchBaseTs])
|
||||
|
||||
|
||||
// --- Intervall erst nach Mount starten, an Sekundengrenze ausrichten ---
|
||||
useEffect(() => {
|
||||
if (!mounted) return
|
||||
if (!Number.isFinite(openTs)) { setMsLeft(0); return }
|
||||
|
||||
const update = () => setMsLeft(Math.max(openTs - Date.now(), 0))
|
||||
update()
|
||||
|
||||
const drift = 1000 - (Date.now() % 1000)
|
||||
let intervalId: number | null = null
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
update()
|
||||
intervalId = window.setInterval(update, 1000)
|
||||
}, drift)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId)
|
||||
if (intervalId) window.clearInterval(intervalId)
|
||||
}
|
||||
}, [openTs, mounted])
|
||||
|
||||
// --- Ab hier nur noch msLeft benutzen ---
|
||||
const isOpen = mounted && msLeft <= 0
|
||||
const msToOpen = msLeft
|
||||
|
||||
/* -------- Admin-Edit Mirror -------- */
|
||||
const adminEditingBy = state?.adminEdit?.by ?? null
|
||||
@ -125,14 +236,6 @@ export default function MapVotePanel({ match }: Props) {
|
||||
}, [adminEditingEnabled, adminEditingBy, session?.user?.steamId])
|
||||
|
||||
/* -------- Derived flags & memoized maps -------- */
|
||||
const opensAt = useMemo(
|
||||
() => (state?.opensAt ? new Date(state.opensAt).getTime() : null),
|
||||
[state?.opensAt]
|
||||
)
|
||||
const isOpenFromMatch = nowTs >= opensAtTs
|
||||
const isOpen = opensAt != null ? nowTs >= opensAt : isOpenFromMatch
|
||||
const msToOpen = Math.max((opensAt ?? opensAtTs) - nowTs, 0)
|
||||
|
||||
const me = session?.user
|
||||
const isAdmin = !!me?.isAdmin
|
||||
const mySteamId = me?.steamId
|
||||
@ -477,7 +580,7 @@ export default function MapVotePanel({ match }: Props) {
|
||||
</div>
|
||||
|
||||
{/* Countdown / Status */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-4" key={openTs}>
|
||||
<div className="mx-auto w-full max-w-xl">
|
||||
<div className="grid grid-cols-[max-content_1fr_max-content] items-center gap-2">
|
||||
<div className="w-10 h-10" />
|
||||
@ -519,8 +622,11 @@ export default function MapVotePanel({ match }: Props) {
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<span className="block text-sm sm:text-base md:text-lg leading-tight whitespace-normal font-semibold px-2 py-1 sm:px-3 sm:py-2 rounded bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100 text-center">
|
||||
Öffnet in {fmtCountdown(msToOpen)}
|
||||
<span
|
||||
className="block text-sm sm:text-base md:text-lg leading-tight whitespace-normal font-semibold px-2 py-1 sm:px-3 sm:py-2 rounded bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100 text-center"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
Öffnet in {mounted ? fmtCountdown(msToOpen) : '–:–:–'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
/app/components/MatchDetails.tsx
|
||||
- Zeigt pro Team einen eigenen „Spieler bearbeiten“-Button
|
||||
- Öffnet das Modal nur für das angeklickte Team
|
||||
- Reagiert auf SSE-Events (match-lineup-updated / matches-updated)
|
||||
─────────────────────────────────────────────────────────────────*/
|
||||
// /app/components/MatchDetails.tsx
|
||||
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
@ -48,6 +44,107 @@ const adr = (dmg?: number, rounds?: number) =>
|
||||
const normalizeMapKey = (raw?: string) =>
|
||||
(raw ?? '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||
|
||||
type VoteAction = 'BAN' | 'PICK' | 'DECIDER'
|
||||
type VoteStep = { order: number; action: VoteAction; map?: string | null }
|
||||
|
||||
const mapLabelFromKey = (key?: string) => {
|
||||
const k = (key ?? '').toLowerCase().replace(/\.bsp$/,'').replace(/^.*\//,'')
|
||||
return (
|
||||
MAP_OPTIONS.find(o => o.key === k)?.label ??
|
||||
(k ? k : 'TBD')
|
||||
)
|
||||
}
|
||||
|
||||
// Maps aus dem MapVote (nur PICK/DECIDER, sortiert)
|
||||
function extractSeriesMaps(match: Match): string[] {
|
||||
const steps = (match.mapVote?.steps ?? []) as unknown as VoteStep[]
|
||||
const picks = steps
|
||||
.filter(s => s && (s.action === 'PICK' || s.action === 'DECIDER'))
|
||||
.sort((a,b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
.map(s => s.map ?? '')
|
||||
// auf bestOf begrenzen
|
||||
const n = Math.max(1, match.bestOf ?? 1)
|
||||
return picks.slice(0, n)
|
||||
}
|
||||
|
||||
function SeriesStrip({
|
||||
bestOf,
|
||||
scoreA = 0,
|
||||
scoreB = 0,
|
||||
maps,
|
||||
}: {
|
||||
bestOf: number
|
||||
scoreA?: number | null
|
||||
scoreB?: number | null
|
||||
maps: string[]
|
||||
}) {
|
||||
const winsA = Math.max(0, scoreA ?? 0)
|
||||
const winsB = Math.max(0, scoreB ?? 0)
|
||||
const needed = Math.ceil(bestOf / 2)
|
||||
const total = Math.max(bestOf, maps.length || 1)
|
||||
|
||||
// index der "aktuellen" Map: sobald jemand fertig ist → keine aktuelle Markierung
|
||||
const finished = winsA >= needed || winsB >= needed
|
||||
const currentIdx = finished ? -1 : Math.min(winsA + winsB, total - 1)
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Kopfzeile der Serie */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-gray-400">
|
||||
Best of {bestOf} • First to {needed}
|
||||
</div>
|
||||
<div className="text-sm font-semibold">
|
||||
{winsA}:{winsB}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kartenleiste */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{Array.from({ length: total }).map((_, i) => {
|
||||
const key = maps[i] ?? ''
|
||||
const label = mapLabelFromKey(key)
|
||||
|
||||
// Siegerbadge pro Map (heuristisch): mapsiegereihenfolge = Sum wins so far?
|
||||
// Da wir kein per-Map Ergebnis haben, markieren wir nur globalen Fortschritt:
|
||||
const isDone = i < winsA + winsB
|
||||
const isCurrent = i === currentIdx
|
||||
const isFuture = i > winsA + winsB
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`series-map-${i}`}
|
||||
className={[
|
||||
'rounded-md px-3 py-2 border flex items-center justify-between',
|
||||
isCurrent
|
||||
? 'border-blue-500 ring-2 ring-blue-300/50 bg-blue-500/10'
|
||||
: isDone
|
||||
? 'border-emerald-500 bg-emerald-500/10'
|
||||
: 'border-gray-600 bg-neutral-800/40',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-xs opacity-70 shrink-0">Map {i + 1}</span>
|
||||
<span className="font-medium truncate">{label}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{isDone && (
|
||||
<span className="text-emerald-400 text-xs font-semibold">✔</span>
|
||||
)}
|
||||
{isCurrent && !isDone && !isFuture && (
|
||||
<span className="text-blue-400 text-[11px] font-semibold">LIVE / als Nächstes</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* ─────────────────── Komponente ─────────────────────────────── */
|
||||
export function MatchDetails({ match, initialNow }: { match: Match; initialNow: number }) {
|
||||
const { data: session } = useSession()
|
||||
@ -56,6 +153,9 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
||||
const isAdmin = !!session?.user?.isAdmin
|
||||
const [now, setNow] = useState(initialNow)
|
||||
const [editMetaOpen, setEditMetaOpen] = useState(false)
|
||||
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null);
|
||||
const [leadOverride, setLeadOverride] = useState<number | null>(null);
|
||||
|
||||
|
||||
/* ─── Rollen & Rechte ─────────────────────────────────────── */
|
||||
const me = session?.user
|
||||
@ -79,6 +179,13 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
||||
const dateString = match.matchDate ?? match.demoDate
|
||||
const readableDate = dateString ? format(new Date(dateString), 'PPpp', { locale: de }) : 'Unbekannt'
|
||||
|
||||
const seriesMaps = useMemo(() => {
|
||||
const fromVote = extractSeriesMaps(match)
|
||||
const n = Math.max(1, match.bestOf ?? 1)
|
||||
return fromVote.length >= n ? fromVote : [...fromVote, ...Array.from({ length: n - fromVote.length }, () => '')]
|
||||
}, [match.bestOf, match.mapVote?.steps?.length])
|
||||
|
||||
|
||||
/* ─── Modal-State: null = geschlossen, 'A' / 'B' = offen ─── */
|
||||
const [editSide, setEditSide] = useState<EditSide | null>(null)
|
||||
|
||||
@ -88,12 +195,22 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
|
||||
// Basiszeit des Matches einmal berechnen
|
||||
const matchBaseTs = useMemo(() => {
|
||||
const raw = match.matchDate ?? match.demoDate ?? initialNow;
|
||||
return new Date(raw).getTime();
|
||||
}, [match.matchDate, match.demoDate, initialNow]);
|
||||
|
||||
const voteOpensAtTs = useMemo(() => {
|
||||
const base = match.mapVote?.opensAt
|
||||
? new Date(match.mapVote.opensAt).getTime()
|
||||
: new Date(match.matchDate ?? match.demoDate ?? initialNow).getTime() - 60 * 60 * 1000
|
||||
return base
|
||||
}, [match.mapVote?.opensAt, match.matchDate, match.demoDate, initialNow])
|
||||
if (opensAtOverride != null) return opensAtOverride; // SSE hat Vorrang
|
||||
if (match.mapVote?.opensAt) return new Date(match.mapVote.opensAt).getTime(); // vom Server
|
||||
const lead = (leadOverride != null) ? leadOverride : 60; // kein 60-min Zwang
|
||||
return matchBaseTs - lead * 60_000;
|
||||
}, [opensAtOverride, match.mapVote?.opensAt, matchBaseTs, leadOverride]);
|
||||
|
||||
const sseOpensAtTs = voteOpensAtTs;
|
||||
const sseLeadMinutes = leadOverride;
|
||||
|
||||
const endDate = new Date(voteOpensAtTs)
|
||||
const mapvoteStarted = (match.mapVote?.isOpen ?? false) || now >= voteOpensAtTs
|
||||
@ -103,19 +220,37 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
||||
|
||||
/* ─── SSE-Listener ─────────────────────────────────────────── */
|
||||
useEffect(() => {
|
||||
if (!lastEvent) return
|
||||
if (!lastEvent) return;
|
||||
const evt = (lastEvent as any).payload ?? lastEvent;
|
||||
if (evt?.matchId !== match.id) return;
|
||||
|
||||
// Match gelöscht? → zurück zur Liste
|
||||
if (lastEvent.type === 'match-deleted' && lastEvent.payload?.matchId === match.id) {
|
||||
router.replace('/schedule')
|
||||
return
|
||||
if (lastEvent.type === 'map-vote-updated') {
|
||||
// opensAt aus Event übernehmen
|
||||
if (evt?.opensAt) {
|
||||
const ts = typeof evt.opensAt === 'string'
|
||||
? new Date(evt.opensAt).getTime()
|
||||
: new Date(evt.opensAt).getTime();
|
||||
setOpensAtOverride(ts);
|
||||
}
|
||||
// leadMinutes mitschneiden u. ggf. opensAt daraus ableiten
|
||||
if (Number.isFinite(evt?.leadMinutes)) {
|
||||
const lead = Number(evt.leadMinutes);
|
||||
setLeadOverride(lead);
|
||||
if (!evt?.opensAt) {
|
||||
const base = new Date(match.matchDate ?? match.demoDate ?? initialNow).getTime();
|
||||
setOpensAtOverride(base - lead * 60_000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Match-Events → Seite frisch rendern
|
||||
if (MATCH_EVENTS.has(lastEvent.type) && lastEvent.payload?.matchId === match.id) {
|
||||
router.refresh()
|
||||
const REFRESH_TYPES = new Set([
|
||||
'map-vote-reset','map-vote-locked','map-vote-unlocked',
|
||||
'match-updated','match-lineup-updated',
|
||||
]);
|
||||
if (REFRESH_TYPES.has(lastEvent.type) && evt?.matchId === match.id) {
|
||||
router.refresh();
|
||||
}
|
||||
}, [lastEvent, match.id, router])
|
||||
}, [lastEvent, match.id, router, match.matchDate, match.demoDate, initialNow]);
|
||||
|
||||
/* ─── Tabellen-Layout (fixe Breiten) ───────────────────────── */
|
||||
const ColGroup = () => (
|
||||
@ -281,7 +416,18 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
||||
|
||||
<p className="text-sm text-gray-500">Datum: {readableDate}</p>
|
||||
|
||||
<div className="text-md">
|
||||
{(match.bestOf ?? 1) > 1 && (
|
||||
<div className="mt-3">
|
||||
<SeriesStrip
|
||||
bestOf={match.bestOf ?? 3}
|
||||
scoreA={match.scoreA}
|
||||
scoreB={match.scoreB}
|
||||
maps={seriesMaps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-md mt-2">
|
||||
<strong>Teams:</strong> {match.teamA?.name ?? 'Unbekannt'} vs. {match.teamB?.name ?? 'Unbekannt'}
|
||||
</div>
|
||||
|
||||
@ -289,7 +435,13 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
||||
<strong>Score:</strong> {match.scoreA ?? 0}:{match.scoreB ?? 0}
|
||||
</div>
|
||||
|
||||
<MapVoteBanner match={match} initialNow={initialNow} />
|
||||
<MapVoteBanner
|
||||
match={match}
|
||||
initialNow={initialNow}
|
||||
matchBaseTs={matchBaseTs}
|
||||
sseOpensAtTs={sseOpensAtTs}
|
||||
sseLeadMinutes={sseLeadMinutes}
|
||||
/>
|
||||
|
||||
{/* ───────── Team-Blöcke ───────── */}
|
||||
<div className="border-t pt-4 mt-4 space-y-10">
|
||||
@ -449,7 +601,7 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
||||
defaultDateISO={match.matchDate ?? match.demoDate ?? null}
|
||||
defaultMap={match.map ?? null}
|
||||
defaultVoteLeadMinutes={60}
|
||||
onSaved={() => { router.refresh() }}
|
||||
onSaved={() => { setTimeout(() => router.refresh(), 0) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -77,6 +77,9 @@ export default function Modal({
|
||||
return () => {
|
||||
modalEl.removeEventListener('hsOverlay:close', handleClose)
|
||||
destroyIfExists()
|
||||
// Fallback: Globale Backdrops wegräumen, falls die Lib zickt
|
||||
document.querySelectorAll('.hs-overlay-backdrop')?.forEach(el => el.remove())
|
||||
document.body.classList.remove('overflow-hidden','[&.hs-overlay-open]') // je nach Lib-Version
|
||||
}
|
||||
}, [show, id, onClose])
|
||||
|
||||
@ -90,7 +93,10 @@ export default function Modal({
|
||||
onMouseDown={(e) => {
|
||||
if (e.target === e.currentTarget) onClose?.()
|
||||
}}
|
||||
className="hs-overlay hidden fixed inset-0 z-80 overflow-y-auto overflow-x-hidden"
|
||||
className={
|
||||
"hs-overlay hidden fixed inset-0 z-80 overflow-y-auto overflow-x-hidden " +
|
||||
(show ? "" : "pointer-events-none")
|
||||
}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 -z-10 bg-black/50 dark:bg-neutral-900/70" />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// sseEvents.ts
|
||||
// /app/lib/sseEvents.ts
|
||||
|
||||
export const SSE_EVENT_TYPES = [
|
||||
// Kanonisch
|
||||
@ -23,6 +23,7 @@ export const SSE_EVENT_TYPES = [
|
||||
'expired-sharecode',
|
||||
'team-invite-revoked',
|
||||
'map-vote-updated',
|
||||
'match-meta-updated',
|
||||
'map-vote-admin-edit',
|
||||
'match-created',
|
||||
'matches-updated',
|
||||
|
||||
@ -41,10 +41,11 @@ export type MapVoteState = {
|
||||
currentIndex: number
|
||||
locked: boolean
|
||||
opensAt: string | null
|
||||
leadMinutes?: number | null
|
||||
steps: MapVoteStep[]
|
||||
teams?: {
|
||||
teamA: MapVoteTeam
|
||||
teamB: MapVoteTeam
|
||||
}
|
||||
adminEdit?: MapVoteAdminEdit // ⬅️ NEU
|
||||
adminEdit?: MapVoteAdminEdit
|
||||
}
|
||||
@ -2,6 +2,15 @@
|
||||
|
||||
import { Player, Team } from './team'
|
||||
|
||||
export type MapVoteStep = {
|
||||
order : number
|
||||
action: 'BAN' | 'PICK' | 'DECIDER'
|
||||
map? : string | null
|
||||
teamId?: string | null
|
||||
chosenAt?: string | null
|
||||
chosenBy?: string | null
|
||||
}
|
||||
|
||||
export type Match = {
|
||||
/* Basis-Infos ---------------------------------------------------- */
|
||||
id : string
|
||||
@ -22,9 +31,11 @@ export type Match = {
|
||||
teamB: Team
|
||||
|
||||
mapVote?: {
|
||||
status: 'not_started' | 'in_progress' | 'completed' | null
|
||||
status : 'not_started' | 'in_progress' | 'completed' | null
|
||||
opensAt: string | null
|
||||
isOpen: boolean | null
|
||||
isOpen : boolean | null
|
||||
locked?: boolean | null
|
||||
steps? : MapVoteStep[]
|
||||
} | null
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -286,6 +286,7 @@ exports.Prisma.MapVoteScalarFieldEnum = {
|
||||
currentIdx: 'currentIdx',
|
||||
locked: 'locked',
|
||||
opensAt: 'opensAt',
|
||||
leadMinutes: 'leadMinutes',
|
||||
adminEditingBy: 'adminEditingBy',
|
||||
adminEditingSince: 'adminEditingSince',
|
||||
createdAt: 'createdAt',
|
||||
|
||||
45
src/generated/prisma/index.d.ts
vendored
45
src/generated/prisma/index.d.ts
vendored
@ -16185,11 +16185,13 @@ export namespace Prisma {
|
||||
export type MapVoteAvgAggregateOutputType = {
|
||||
bestOf: number | null
|
||||
currentIdx: number | null
|
||||
leadMinutes: number | null
|
||||
}
|
||||
|
||||
export type MapVoteSumAggregateOutputType = {
|
||||
bestOf: number | null
|
||||
currentIdx: number | null
|
||||
leadMinutes: number | null
|
||||
}
|
||||
|
||||
export type MapVoteMinAggregateOutputType = {
|
||||
@ -16199,6 +16201,7 @@ export namespace Prisma {
|
||||
currentIdx: number | null
|
||||
locked: boolean | null
|
||||
opensAt: Date | null
|
||||
leadMinutes: number | null
|
||||
adminEditingBy: string | null
|
||||
adminEditingSince: Date | null
|
||||
createdAt: Date | null
|
||||
@ -16212,6 +16215,7 @@ export namespace Prisma {
|
||||
currentIdx: number | null
|
||||
locked: boolean | null
|
||||
opensAt: Date | null
|
||||
leadMinutes: number | null
|
||||
adminEditingBy: string | null
|
||||
adminEditingSince: Date | null
|
||||
createdAt: Date | null
|
||||
@ -16226,6 +16230,7 @@ export namespace Prisma {
|
||||
currentIdx: number
|
||||
locked: number
|
||||
opensAt: number
|
||||
leadMinutes: number
|
||||
adminEditingBy: number
|
||||
adminEditingSince: number
|
||||
createdAt: number
|
||||
@ -16237,11 +16242,13 @@ export namespace Prisma {
|
||||
export type MapVoteAvgAggregateInputType = {
|
||||
bestOf?: true
|
||||
currentIdx?: true
|
||||
leadMinutes?: true
|
||||
}
|
||||
|
||||
export type MapVoteSumAggregateInputType = {
|
||||
bestOf?: true
|
||||
currentIdx?: true
|
||||
leadMinutes?: true
|
||||
}
|
||||
|
||||
export type MapVoteMinAggregateInputType = {
|
||||
@ -16251,6 +16258,7 @@ export namespace Prisma {
|
||||
currentIdx?: true
|
||||
locked?: true
|
||||
opensAt?: true
|
||||
leadMinutes?: true
|
||||
adminEditingBy?: true
|
||||
adminEditingSince?: true
|
||||
createdAt?: true
|
||||
@ -16264,6 +16272,7 @@ export namespace Prisma {
|
||||
currentIdx?: true
|
||||
locked?: true
|
||||
opensAt?: true
|
||||
leadMinutes?: true
|
||||
adminEditingBy?: true
|
||||
adminEditingSince?: true
|
||||
createdAt?: true
|
||||
@ -16278,6 +16287,7 @@ export namespace Prisma {
|
||||
currentIdx?: true
|
||||
locked?: true
|
||||
opensAt?: true
|
||||
leadMinutes?: true
|
||||
adminEditingBy?: true
|
||||
adminEditingSince?: true
|
||||
createdAt?: true
|
||||
@ -16379,6 +16389,7 @@ export namespace Prisma {
|
||||
currentIdx: number
|
||||
locked: boolean
|
||||
opensAt: Date | null
|
||||
leadMinutes: number
|
||||
adminEditingBy: string | null
|
||||
adminEditingSince: Date | null
|
||||
createdAt: Date
|
||||
@ -16412,6 +16423,7 @@ export namespace Prisma {
|
||||
currentIdx?: boolean
|
||||
locked?: boolean
|
||||
opensAt?: boolean
|
||||
leadMinutes?: boolean
|
||||
adminEditingBy?: boolean
|
||||
adminEditingSince?: boolean
|
||||
createdAt?: boolean
|
||||
@ -16429,6 +16441,7 @@ export namespace Prisma {
|
||||
currentIdx?: boolean
|
||||
locked?: boolean
|
||||
opensAt?: boolean
|
||||
leadMinutes?: boolean
|
||||
adminEditingBy?: boolean
|
||||
adminEditingSince?: boolean
|
||||
createdAt?: boolean
|
||||
@ -16444,6 +16457,7 @@ export namespace Prisma {
|
||||
currentIdx?: boolean
|
||||
locked?: boolean
|
||||
opensAt?: boolean
|
||||
leadMinutes?: boolean
|
||||
adminEditingBy?: boolean
|
||||
adminEditingSince?: boolean
|
||||
createdAt?: boolean
|
||||
@ -16459,13 +16473,14 @@ export namespace Prisma {
|
||||
currentIdx?: boolean
|
||||
locked?: boolean
|
||||
opensAt?: boolean
|
||||
leadMinutes?: boolean
|
||||
adminEditingBy?: boolean
|
||||
adminEditingSince?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
}
|
||||
|
||||
export type MapVoteOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "matchId" | "bestOf" | "mapPool" | "currentIdx" | "locked" | "opensAt" | "adminEditingBy" | "adminEditingSince" | "createdAt" | "updatedAt", ExtArgs["result"]["mapVote"]>
|
||||
export type MapVoteOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "matchId" | "bestOf" | "mapPool" | "currentIdx" | "locked" | "opensAt" | "leadMinutes" | "adminEditingBy" | "adminEditingSince" | "createdAt" | "updatedAt", ExtArgs["result"]["mapVote"]>
|
||||
export type MapVoteInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
|
||||
match?: boolean | MatchDefaultArgs<ExtArgs>
|
||||
steps?: boolean | MapVote$stepsArgs<ExtArgs>
|
||||
@ -16492,6 +16507,7 @@ export namespace Prisma {
|
||||
currentIdx: number
|
||||
locked: boolean
|
||||
opensAt: Date | null
|
||||
leadMinutes: number
|
||||
adminEditingBy: string | null
|
||||
adminEditingSince: Date | null
|
||||
createdAt: Date
|
||||
@ -16928,6 +16944,7 @@ export namespace Prisma {
|
||||
readonly currentIdx: FieldRef<"MapVote", 'Int'>
|
||||
readonly locked: FieldRef<"MapVote", 'Boolean'>
|
||||
readonly opensAt: FieldRef<"MapVote", 'DateTime'>
|
||||
readonly leadMinutes: FieldRef<"MapVote", 'Int'>
|
||||
readonly adminEditingBy: FieldRef<"MapVote", 'String'>
|
||||
readonly adminEditingSince: FieldRef<"MapVote", 'DateTime'>
|
||||
readonly createdAt: FieldRef<"MapVote", 'DateTime'>
|
||||
@ -18768,6 +18785,7 @@ export namespace Prisma {
|
||||
currentIdx: 'currentIdx',
|
||||
locked: 'locked',
|
||||
opensAt: 'opensAt',
|
||||
leadMinutes: 'leadMinutes',
|
||||
adminEditingBy: 'adminEditingBy',
|
||||
adminEditingSince: 'adminEditingSince',
|
||||
createdAt: 'createdAt',
|
||||
@ -20066,6 +20084,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFilter<"MapVote"> | number
|
||||
locked?: BoolFilter<"MapVote"> | boolean
|
||||
opensAt?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
||||
leadMinutes?: IntFilter<"MapVote"> | number
|
||||
adminEditingBy?: StringNullableFilter<"MapVote"> | string | null
|
||||
adminEditingSince?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
||||
createdAt?: DateTimeFilter<"MapVote"> | Date | string
|
||||
@ -20082,6 +20101,7 @@ export namespace Prisma {
|
||||
currentIdx?: SortOrder
|
||||
locked?: SortOrder
|
||||
opensAt?: SortOrderInput | SortOrder
|
||||
leadMinutes?: SortOrder
|
||||
adminEditingBy?: SortOrderInput | SortOrder
|
||||
adminEditingSince?: SortOrderInput | SortOrder
|
||||
createdAt?: SortOrder
|
||||
@ -20101,6 +20121,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFilter<"MapVote"> | number
|
||||
locked?: BoolFilter<"MapVote"> | boolean
|
||||
opensAt?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
||||
leadMinutes?: IntFilter<"MapVote"> | number
|
||||
adminEditingBy?: StringNullableFilter<"MapVote"> | string | null
|
||||
adminEditingSince?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
||||
createdAt?: DateTimeFilter<"MapVote"> | Date | string
|
||||
@ -20117,6 +20138,7 @@ export namespace Prisma {
|
||||
currentIdx?: SortOrder
|
||||
locked?: SortOrder
|
||||
opensAt?: SortOrderInput | SortOrder
|
||||
leadMinutes?: SortOrder
|
||||
adminEditingBy?: SortOrderInput | SortOrder
|
||||
adminEditingSince?: SortOrderInput | SortOrder
|
||||
createdAt?: SortOrder
|
||||
@ -20139,6 +20161,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntWithAggregatesFilter<"MapVote"> | number
|
||||
locked?: BoolWithAggregatesFilter<"MapVote"> | boolean
|
||||
opensAt?: DateTimeNullableWithAggregatesFilter<"MapVote"> | Date | string | null
|
||||
leadMinutes?: IntWithAggregatesFilter<"MapVote"> | number
|
||||
adminEditingBy?: StringNullableWithAggregatesFilter<"MapVote"> | string | null
|
||||
adminEditingSince?: DateTimeNullableWithAggregatesFilter<"MapVote"> | Date | string | null
|
||||
createdAt?: DateTimeWithAggregatesFilter<"MapVote"> | Date | string
|
||||
@ -21425,6 +21448,7 @@ export namespace Prisma {
|
||||
currentIdx?: number
|
||||
locked?: boolean
|
||||
opensAt?: Date | string | null
|
||||
leadMinutes?: number
|
||||
adminEditingBy?: string | null
|
||||
adminEditingSince?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
@ -21441,6 +21465,7 @@ export namespace Prisma {
|
||||
currentIdx?: number
|
||||
locked?: boolean
|
||||
opensAt?: Date | string | null
|
||||
leadMinutes?: number
|
||||
adminEditingBy?: string | null
|
||||
adminEditingSince?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
@ -21455,6 +21480,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -21471,6 +21497,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -21486,6 +21513,7 @@ export namespace Prisma {
|
||||
currentIdx?: number
|
||||
locked?: boolean
|
||||
opensAt?: Date | string | null
|
||||
leadMinutes?: number
|
||||
adminEditingBy?: string | null
|
||||
adminEditingSince?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
@ -21499,6 +21527,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -21513,6 +21542,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -22678,6 +22708,7 @@ export namespace Prisma {
|
||||
currentIdx?: SortOrder
|
||||
locked?: SortOrder
|
||||
opensAt?: SortOrder
|
||||
leadMinutes?: SortOrder
|
||||
adminEditingBy?: SortOrder
|
||||
adminEditingSince?: SortOrder
|
||||
createdAt?: SortOrder
|
||||
@ -22687,6 +22718,7 @@ export namespace Prisma {
|
||||
export type MapVoteAvgOrderByAggregateInput = {
|
||||
bestOf?: SortOrder
|
||||
currentIdx?: SortOrder
|
||||
leadMinutes?: SortOrder
|
||||
}
|
||||
|
||||
export type MapVoteMaxOrderByAggregateInput = {
|
||||
@ -22696,6 +22728,7 @@ export namespace Prisma {
|
||||
currentIdx?: SortOrder
|
||||
locked?: SortOrder
|
||||
opensAt?: SortOrder
|
||||
leadMinutes?: SortOrder
|
||||
adminEditingBy?: SortOrder
|
||||
adminEditingSince?: SortOrder
|
||||
createdAt?: SortOrder
|
||||
@ -22709,6 +22742,7 @@ export namespace Prisma {
|
||||
currentIdx?: SortOrder
|
||||
locked?: SortOrder
|
||||
opensAt?: SortOrder
|
||||
leadMinutes?: SortOrder
|
||||
adminEditingBy?: SortOrder
|
||||
adminEditingSince?: SortOrder
|
||||
createdAt?: SortOrder
|
||||
@ -22718,6 +22752,7 @@ export namespace Prisma {
|
||||
export type MapVoteSumOrderByAggregateInput = {
|
||||
bestOf?: SortOrder
|
||||
currentIdx?: SortOrder
|
||||
leadMinutes?: SortOrder
|
||||
}
|
||||
|
||||
export type EnumMapVoteActionFilter<$PrismaModel = never> = {
|
||||
@ -26846,6 +26881,7 @@ export namespace Prisma {
|
||||
currentIdx?: number
|
||||
locked?: boolean
|
||||
opensAt?: Date | string | null
|
||||
leadMinutes?: number
|
||||
adminEditingBy?: string | null
|
||||
adminEditingSince?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
@ -26860,6 +26896,7 @@ export namespace Prisma {
|
||||
currentIdx?: number
|
||||
locked?: boolean
|
||||
opensAt?: Date | string | null
|
||||
leadMinutes?: number
|
||||
adminEditingBy?: string | null
|
||||
adminEditingSince?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
@ -27108,6 +27145,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -27122,6 +27160,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -29149,6 +29188,7 @@ export namespace Prisma {
|
||||
currentIdx?: number
|
||||
locked?: boolean
|
||||
opensAt?: Date | string | null
|
||||
leadMinutes?: number
|
||||
adminEditingBy?: string | null
|
||||
adminEditingSince?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
@ -29164,6 +29204,7 @@ export namespace Prisma {
|
||||
currentIdx?: number
|
||||
locked?: boolean
|
||||
opensAt?: Date | string | null
|
||||
leadMinutes?: number
|
||||
adminEditingBy?: string | null
|
||||
adminEditingSince?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
@ -29303,6 +29344,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -29318,6 +29360,7 @@ export namespace Prisma {
|
||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "prisma-client-6d46bb441ad6771c9d4c18422a89623b795f3a4d205bdd0a7fbf78b6d9d34ce6",
|
||||
"name": "prisma-client-da0847ed2b650f4e980bae112054fc21d3b01519299b5975e3d05dad9fd53b68",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js",
|
||||
|
||||
@ -323,6 +323,8 @@ model MapVote {
|
||||
locked Boolean @default(false)
|
||||
opensAt DateTime?
|
||||
|
||||
leadMinutes Int @default(60)
|
||||
|
||||
adminEditingBy String?
|
||||
adminEditingSince DateTime?
|
||||
|
||||
|
||||
@ -286,6 +286,7 @@ exports.Prisma.MapVoteScalarFieldEnum = {
|
||||
currentIdx: 'currentIdx',
|
||||
locked: 'locked',
|
||||
opensAt: 'opensAt',
|
||||
leadMinutes: 'leadMinutes',
|
||||
adminEditingBy: 'adminEditingBy',
|
||||
adminEditingSince: 'adminEditingSince',
|
||||
createdAt: 'createdAt',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user