Compare commits
No commits in common. "728b5cb6f64232a011c4c08cb40308b81a80f2d1" and "36745d2ae5274662fce0dc4c111c7c8bf64d7503" have entirely different histories.
728b5cb6f6
...
36745d2ae5
78
package-lock.json
generated
78
package-lock.json
generated
@ -15,7 +15,7 @@
|
|||||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||||
"@preline/dropdown": "^3.0.1",
|
"@preline/dropdown": "^3.0.1",
|
||||||
"@preline/tooltip": "^3.0.0",
|
"@preline/tooltip": "^3.0.0",
|
||||||
"@prisma/client": "^6.15.0",
|
"@prisma/client": "^6.14.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"csgo-sharecode": "^3.1.2",
|
"csgo-sharecode": "^3.1.2",
|
||||||
@ -61,7 +61,7 @@
|
|||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.0",
|
"eslint-config-next": "15.3.0",
|
||||||
"prisma": "^6.15.0",
|
"prisma": "^6.14.0",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.19.4",
|
||||||
@ -1599,9 +1599,9 @@
|
|||||||
"license": "Licensed under MIT and Preline UI Fair Use License"
|
"license": "Licensed under MIT and Preline UI Fair Use License"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "6.15.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.14.0.tgz",
|
||||||
"integrity": "sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==",
|
"integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -1621,9 +1621,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/config": {
|
"node_modules/@prisma/config": {
|
||||||
"version": "6.15.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.14.0.tgz",
|
||||||
"integrity": "sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==",
|
"integrity": "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1634,53 +1634,53 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/debug": {
|
"node_modules/@prisma/debug": {
|
||||||
"version": "6.15.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.14.0.tgz",
|
||||||
"integrity": "sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==",
|
"integrity": "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.15.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.14.0.tgz",
|
||||||
"integrity": "sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==",
|
"integrity": "sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.15.0",
|
"@prisma/debug": "6.14.0",
|
||||||
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
|
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||||
"@prisma/fetch-engine": "6.15.0",
|
"@prisma/fetch-engine": "6.14.0",
|
||||||
"@prisma/get-platform": "6.15.0"
|
"@prisma/get-platform": "6.14.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
|
"version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz",
|
||||||
"integrity": "sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==",
|
"integrity": "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.15.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz",
|
||||||
"integrity": "sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==",
|
"integrity": "sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.15.0",
|
"@prisma/debug": "6.14.0",
|
||||||
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
|
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||||
"@prisma/get-platform": "6.15.0"
|
"@prisma/get-platform": "6.14.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "6.15.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.14.0.tgz",
|
||||||
"integrity": "sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==",
|
"integrity": "sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.15.0"
|
"@prisma/debug": "6.14.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
@ -6663,9 +6663,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pkg-types": {
|
"node_modules/pkg-types": {
|
||||||
"version": "2.3.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz",
|
||||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
"integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -6781,15 +6781,15 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "6.15.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.14.0.tgz",
|
||||||
"integrity": "sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==",
|
"integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "6.15.0",
|
"@prisma/config": "6.14.0",
|
||||||
"@prisma/engines": "6.15.0"
|
"@prisma/engines": "6.14.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js"
|
"prisma": "build/index.js"
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||||
"@preline/dropdown": "^3.0.1",
|
"@preline/dropdown": "^3.0.1",
|
||||||
"@preline/tooltip": "^3.0.0",
|
"@preline/tooltip": "^3.0.0",
|
||||||
"@prisma/client": "^6.15.0",
|
"@prisma/client": "^6.14.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"csgo-sharecode": "^3.1.2",
|
"csgo-sharecode": "^3.1.2",
|
||||||
@ -65,7 +65,7 @@
|
|||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.0",
|
"eslint-config-next": "15.3.0",
|
||||||
"prisma": "^6.15.0",
|
"prisma": "^6.14.0",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.19.4",
|
||||||
|
|||||||
@ -324,8 +324,6 @@ model MapVote {
|
|||||||
locked Boolean @default(false)
|
locked Boolean @default(false)
|
||||||
opensAt DateTime?
|
opensAt DateTime?
|
||||||
|
|
||||||
leadMinutes Int @default(60)
|
|
||||||
|
|
||||||
adminEditingBy String?
|
adminEditingBy String?
|
||||||
adminEditingSince DateTime?
|
adminEditingSince DateTime?
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@ function buildSteps(bestOf: number, teamAId: string, teamBId: string) {
|
|||||||
] as const
|
] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
function shapeState(vote: any, match?: any) {
|
function shapeState(vote: any) {
|
||||||
const steps = [...vote.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) => ({
|
||||||
@ -78,24 +78,20 @@ function shapeState(vote: any, match?: any) {
|
|||||||
chosenBy: s.chosenBy ?? null,
|
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 {
|
return {
|
||||||
bestOf : vote.bestOf,
|
bestOf : vote.bestOf,
|
||||||
mapPool : vote.mapPool as string[],
|
mapPool : vote.mapPool as string[],
|
||||||
currentIndex: vote.currentIdx,
|
currentIndex: vote.currentIdx,
|
||||||
locked : vote.locked as boolean,
|
locked : vote.locked as boolean,
|
||||||
opensAt : opensAtDate ? opensAtDate.toISOString() : null,
|
opensAt : vote.opensAt ? new Date(vote.opensAt).toISOString() : null,
|
||||||
leadMinutes,
|
|
||||||
steps,
|
steps,
|
||||||
|
// Admin-Edit Shape
|
||||||
adminEdit: vote.adminEditingBy
|
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 },
|
: { enabled: false, by: null, since: null },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -422,7 +418,7 @@ function deriveChosenSteps(vote: any) {
|
|||||||
export async function GET(req: NextRequest, { params }: { params: { matchId: string } }) {
|
export async function GET(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||||
try {
|
try {
|
||||||
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 id' }, { status: 400 })
|
||||||
|
|
||||||
const { match, vote } = await ensureVote(matchId)
|
const { match, vote } = await ensureVote(matchId)
|
||||||
if (!match || !vote) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 })
|
if (!match || !vote) return NextResponse.json({ message: 'Match nicht gefunden' }, { status: 404 })
|
||||||
@ -431,7 +427,7 @@ export async function GET(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
const mapVisuals = buildMapVisuals(match.id, vote.mapPool)
|
const mapVisuals = buildMapVisuals(match.id, vote.mapPool)
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ ...shapeState(vote, match), mapVisuals, teams }, // 👈 match mitgeben
|
{ ...shapeState(vote), mapVisuals, teams },
|
||||||
{ headers: { 'Cache-Control': 'no-store' } },
|
{ headers: { 'Cache-Control': 'no-store' } },
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -466,15 +462,12 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
|
|
||||||
const updated = await setAdminEdit(vote.id, body.adminEdit ? me.steamId : null)
|
const updated = await setAdminEdit(vote.id, body.adminEdit ? me.steamId : null)
|
||||||
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||||
type: 'map-vote-updated',
|
|
||||||
payload: { matchId, opensAt: updated.opensAt ?? null },
|
|
||||||
})
|
|
||||||
|
|
||||||
const teams = buildTeamsPayloadFromMatch(match)
|
const teams = buildTeamsPayloadFromMatch(match)
|
||||||
const mapVisuals = buildMapVisuals(match.id, updated.mapPool)
|
const mapVisuals = buildMapVisuals(match.id, updated.mapPool)
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------- Wenn anderer Admin editiert: Voting sperren -------- */
|
/* -------- Wenn anderer Admin editiert: Voting sperren -------- */
|
||||||
@ -499,10 +492,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
await prisma.mapVote.update({ where: { id: vote.id }, data: { locked: true } })
|
await prisma.mapVote.update({ where: { id: vote.id }, data: { locked: true } })
|
||||||
const updated = await prisma.mapVote.findUnique({ where: { id: vote.id }, include: { steps: true } })
|
const updated = await prisma.mapVote.findUnique({ where: { id: vote.id }, include: { steps: true } })
|
||||||
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||||
type: 'map-vote-updated',
|
|
||||||
payload: { matchId, opensAt: updated?.opensAt ?? null },
|
|
||||||
})
|
|
||||||
|
|
||||||
const teams = buildTeamsPayloadFromMatch(match)
|
const teams = buildTeamsPayloadFromMatch(match)
|
||||||
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||||
@ -529,7 +519,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
await exportMatchToSftpDirect(match, updated)
|
await exportMatchToSftpDirect(match, updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||||
}
|
}
|
||||||
|
|
||||||
const available = computeAvailableMaps(vote.mapPool, stepsSorted)
|
const available = computeAvailableMaps(vote.mapPool, stepsSorted)
|
||||||
@ -556,10 +546,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
include: { steps: true },
|
include: { steps: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||||
type: 'map-vote-updated',
|
|
||||||
payload: { matchId, opensAt: updated?.opensAt ?? null },
|
|
||||||
})
|
|
||||||
|
|
||||||
const teams = buildTeamsPayloadFromMatch(match)
|
const teams = buildTeamsPayloadFromMatch(match)
|
||||||
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||||
@ -586,7 +573,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
await exportMatchToSftpDirect(match, updated)
|
await exportMatchToSftpDirect(match, updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
return NextResponse.json({ ...shapeState(updated), mapVisuals, teams })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rechte prüfen (Admin oder Leader des Teams am Zug)
|
// Rechte prüfen (Admin oder Leader des Teams am Zug)
|
||||||
@ -652,10 +639,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
include: { steps: true },
|
include: { steps: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({ type: 'map-vote-updated', matchId })
|
||||||
type: 'map-vote-updated',
|
|
||||||
payload: { matchId, opensAt: updated?.opensAt ?? null },
|
|
||||||
})
|
|
||||||
|
|
||||||
const teams = buildTeamsPayloadFromMatch(match)
|
const teams = buildTeamsPayloadFromMatch(match)
|
||||||
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||||
@ -681,7 +665,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
await exportMatchToSftpDirect(match, updated)
|
await exportMatchToSftpDirect(match, updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, 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 })
|
||||||
|
|||||||
@ -1,76 +1,18 @@
|
|||||||
// /app/api/matches/[matchId]/meta/route.ts
|
// /app/api/matches/[id]/meta/route.ts
|
||||||
import { NextResponse, type NextRequest } from 'next/server'
|
import { NextResponse, type NextRequest } from 'next/server'
|
||||||
import { prisma } from '@/app/lib/prisma'
|
import { prisma } from '@/app/lib/prisma'
|
||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/app/lib/auth'
|
import { authOptions } from '@/app/lib/auth'
|
||||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||||
|
|
||||||
export const runtime = 'nodejs' // 👈 wie bei mapvote
|
export async function PUT(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||||
export const dynamic = 'force-dynamic' // 👈 wie bei mapvote
|
const id = params.matchId
|
||||||
|
|
||||||
// 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 session = await getServerSession(authOptions(req))
|
||||||
const me = session?.user
|
const me = session?.user
|
||||||
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 {
|
const { title, matchType, teamAId, teamBId, matchDate, map, voteLeadMinutes } = body ?? {}
|
||||||
title,
|
|
||||||
matchType,
|
|
||||||
teamAId,
|
|
||||||
teamBId,
|
|
||||||
matchDate,
|
|
||||||
map,
|
|
||||||
voteLeadMinutes, // optional
|
|
||||||
demoDate,
|
|
||||||
} = body ?? {}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const match = await prisma.match.findUnique({
|
const match = await prisma.match.findUnique({
|
||||||
@ -90,38 +32,24 @@ export async function PUT(
|
|||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Matching-Daten zusammenbauen
|
|
||||||
const updateData: any = {}
|
const updateData: any = {}
|
||||||
if (typeof title !== 'undefined') updateData.title = title
|
if (typeof title !== 'undefined') updateData.title = title
|
||||||
if (typeof matchType === 'string') updateData.matchType = matchType
|
if (typeof matchType === 'string') updateData.matchType = matchType
|
||||||
if (typeof map !== 'undefined') updateData.map = map
|
if (typeof map !== 'undefined') updateData.map = map
|
||||||
if (typeof teamAId !== 'undefined') updateData.teamAId = teamAId
|
if (typeof teamAId !== 'undefined') updateData.teamAId = teamAId
|
||||||
if (typeof teamBId !== 'undefined') updateData.teamBId = teamBId
|
if (typeof teamBId !== 'undefined') updateData.teamBId = teamBId
|
||||||
|
if (typeof matchDate === 'string' || matchDate === null) {
|
||||||
const parsedMatchDate = parseDateOrNull(matchDate)
|
updateData.matchDate = matchDate ? new Date(matchDate) : null
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Lead bestimmen (Body > gespeicherter Wert > default 60)
|
const lead = Number.isFinite(Number(voteLeadMinutes)) ? Number(voteLeadMinutes) : 60
|
||||||
const leadBodyRaw = Number(voteLeadMinutes)
|
let opensAt: Date | null = null
|
||||||
const leadBody = Number.isFinite(leadBodyRaw) ? leadBodyRaw : undefined
|
if (updateData.matchDate instanceof Date) {
|
||||||
const currentLead = match.mapVote?.leadMinutes ?? 60 // erfordert Feld im Schema
|
opensAt = new Date(updateData.matchDate.getTime() - lead * 60 * 1000)
|
||||||
const leadMinutes = leadBody ?? currentLead
|
} else if (match.matchDate) {
|
||||||
|
opensAt = new Date(match.matchDate.getTime() - lead * 60 * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
// 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 updated = await prisma.$transaction(async (tx) => {
|
||||||
const m = await tx.match.update({
|
const m = await tx.match.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
@ -129,34 +57,21 @@ export async function PUT(
|
|||||||
include: { mapVote: true },
|
include: { mapVote: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wenn wir eine Basiszeit haben → opensAt neu berechnen
|
if (opensAt) {
|
||||||
if (baseDate) {
|
|
||||||
const opensAt = voteOpensAt(baseDate, leadMinutes)
|
|
||||||
|
|
||||||
if (!m.mapVote) {
|
if (!m.mapVote) {
|
||||||
// MapVote existiert noch nicht → nur opensAt/leadMinutes anlegen (Schritte erstellt get /mapvote)
|
|
||||||
await tx.mapVote.create({
|
await tx.mapVote.create({
|
||||||
data: {
|
data: {
|
||||||
matchId: m.id,
|
matchId: m.id,
|
||||||
leadMinutes: leadMinutes,
|
|
||||||
opensAt,
|
opensAt,
|
||||||
|
// entferne Felder, die es im Schema nicht gibt (z. B. isOpen)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await tx.mapVote.update({
|
await tx.mapVote.update({
|
||||||
where: { id: m.mapVote.id },
|
where: { id: m.mapVote.id },
|
||||||
data: {
|
data: { opensAt },
|
||||||
...(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({
|
return tx.match.findUnique({
|
||||||
@ -171,32 +86,16 @@ export async function PUT(
|
|||||||
|
|
||||||
if (!updated) return NextResponse.json({ error: 'Reload failed' }, { status: 500 })
|
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({
|
await sendServerSSEMessage({
|
||||||
type: 'match-updated',
|
type: 'match-updated',
|
||||||
payload: { matchId: updated.id, updatedAt: new Date().toISOString() },
|
payload: { matchId: updated.id, updatedAt: new Date().toISOString() },
|
||||||
})
|
})
|
||||||
|
|
||||||
// 6) Response (no-store)
|
await sendServerSSEMessage({
|
||||||
|
type: 'map-vote-updated',
|
||||||
|
payload: { matchId: updated.id, opensAt: updated.mapVote?.opensAt ?? null },
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id: updated.id,
|
id: updated.id,
|
||||||
title: updated.title,
|
title: updated.title,
|
||||||
@ -204,7 +103,6 @@ export async function PUT(
|
|||||||
teamAId: updated.teamAId,
|
teamAId: updated.teamAId,
|
||||||
teamBId: updated.teamBId,
|
teamBId: updated.teamBId,
|
||||||
matchDate: updated.matchDate,
|
matchDate: updated.matchDate,
|
||||||
demoDate: updated.demoDate,
|
|
||||||
map: updated.map,
|
map: updated.map,
|
||||||
mapVote: updated.mapVote,
|
mapVote: updated.mapVote,
|
||||||
}, { headers: { 'Cache-Control': 'no-store' } })
|
}, { headers: { 'Cache-Control': 'no-store' } })
|
||||||
|
|||||||
@ -24,14 +24,6 @@ export async function GET(_: Request, { params }: { params: { matchId: string }
|
|||||||
players: { include: { user: true, stats: true, team: true } },
|
players: { include: { user: true, stats: true, team: true } },
|
||||||
teamAUsers: { include: { team: true } },
|
teamAUsers: { include: { team: true } },
|
||||||
teamBUsers: { 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 })
|
if (!m) return NextResponse.json({ error: 'Match nicht gefunden' }, { status: 404 })
|
||||||
@ -39,24 +31,9 @@ export async function GET(_: Request, { params }: { params: { matchId: string }
|
|||||||
const payload =
|
const payload =
|
||||||
m.matchType === 'community' && isFuture(m)
|
m.matchType === 'community' && isFuture(m)
|
||||||
? await buildCommunityFuturePayload(m)
|
? await buildCommunityFuturePayload(m)
|
||||||
: buildDefaultPayload(m);
|
: buildDefaultPayload(m)
|
||||||
|
|
||||||
// ⬇️ Zusatz: opensAt (und leadMinutes) an die Antwort hängen
|
return NextResponse.json(payload, { headers: { 'Cache-Control': 'no-store' } })
|
||||||
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) {
|
} catch (err) {
|
||||||
console.error(`GET /matches/${params.matchId} failed:`, err)
|
console.error(`GET /matches/${params.matchId} failed:`, err)
|
||||||
return NextResponse.json({ error: 'Failed to load match' }, { status: 500 })
|
return NextResponse.json({ error: 'Failed to load match' }, { status: 500 })
|
||||||
@ -207,14 +184,6 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
|||||||
players: { include: { user: true, stats: true, team: true } },
|
players: { include: { user: true, stats: true, team: true } },
|
||||||
teamAUsers: { include: { team: true } },
|
teamAUsers: { include: { team: true } },
|
||||||
teamBUsers: { 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 })
|
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 { useRouter } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { format } from 'date-fns' // 👈 neu
|
import { format } from 'date-fns'
|
||||||
import { de } from 'date-fns/locale'
|
import { de } from 'date-fns/locale'
|
||||||
import Switch from '@/app/components/Switch'
|
import Switch from '@/app/components/Switch'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
@ -41,20 +41,6 @@ function getNextHourDefaults() {
|
|||||||
return { dateStr, timeStr }
|
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) {
|
export default function CommunityMatchList({ matchType }: Props) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -85,13 +71,6 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
[teams]
|
[teams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [now, setNow] = useState(() => Date.now())
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const id = setInterval(() => setNow(Date.now()), 1000)
|
|
||||||
return () => clearInterval(id)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
// force re-render, damit isOpen (Vergleich mit Date.now) neu bewertet wird
|
// force re-render, damit isOpen (Vergleich mit Date.now) neu bewertet wird
|
||||||
@ -273,85 +252,52 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
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
|
||||||
const isOwnTeam = !!session?.user?.team &&
|
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
|
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 (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={m.id}
|
key={m.id}
|
||||||
href={`/match-details/${m.id}`}
|
href={`/match-details/${m.id}`}
|
||||||
className={`
|
className={`
|
||||||
grid grid-rows-[auto_1fr_auto] justify-items-center gap-3
|
relative flex flex-col items-center gap-4 bg-neutral-300 dark:bg-neutral-800
|
||||||
bg-neutral-300 dark:bg-neutral-800 text-gray-800 dark:text-white
|
text-gray-800 dark:text-white rounded-sm py-4
|
||||||
rounded-sm p-4 min-h-[210px]
|
hover:scale-105 hover:bg-neutral-400 hover:dark:bg-neutral-700
|
||||||
hover:scale-105 hover:bg-neutral-400 hover:dark:bg-neutral-700 hover:shadow-md
|
hover:shadow-md h-[172px]
|
||||||
transition-transform transition-opacity duration-300 ease-in-out
|
transition-transform transition-opacity duration-300 ease-in-out
|
||||||
${dimmed ? 'opacity-40' : 'opacity-100'}
|
${dimmed ? 'opacity-40' : 'opacity-100'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{/* Zeile 1: Badges (immer Platz, auch wenn leer) */}
|
{/* Live / Map-Vote Badge */}
|
||||||
<div className="flex flex-col items-center gap-1 min-h-6">
|
{isLive ? (
|
||||||
{isLive ? (
|
<span className="absolute top-2 px-2 py-0.5 text-xs font-semibold rounded-full bg-red-500 text-white shadow">
|
||||||
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-red-500 text-white shadow">
|
LIVE
|
||||||
LIVE
|
</span>
|
||||||
</span>
|
) : (m.mapVote?.isOpen ? (
|
||||||
) : mv.hasVote && (mv.isOpen || opensText) ? (
|
<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
|
||||||
{mv.isOpen && (
|
</span>
|
||||||
<span className="px-2 py-0.5 text-[11px] font-semibold rounded-full bg-green-600 text-white shadow">
|
) : null
|
||||||
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 w-full justify-around items-center">
|
||||||
<div className="flex flex-col items-center w-1/3">
|
<div className="flex flex-col items-center w-1/3">
|
||||||
<Image
|
<Image src={getTeamLogo(m.teamA.logo)} alt={m.teamA.name} width={48} height={48} className="rounded-full border bg-white" />
|
||||||
src={getTeamLogo(m.teamA.logo)}
|
<span className="mt-2 text-xs">{m.teamA.name}</span>
|
||||||
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>
|
</div>
|
||||||
<span className="font-bold">vs</span>
|
<span className="font-bold">vs</span>
|
||||||
<div className="flex flex-col items-center w-1/3">
|
<div className="flex flex-col items-center w-1/3">
|
||||||
<Image
|
<Image src={getTeamLogo(m.teamB.logo)} alt={m.teamB.name} width={48} height={48} className="rounded-full border bg-white" />
|
||||||
src={getTeamLogo(m.teamB.logo)}
|
<span className="mt-2 text-xs">{m.teamB.name}</span>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zeile 3: Datum & Uhrzeit */}
|
{/* Datum + Uhrzeit: höher & highlight */}
|
||||||
<div className="flex flex-col items-center -mt-1 space-y-1">
|
<div className="flex flex-col items-center -mt-1 space-y-1">
|
||||||
<span
|
<span
|
||||||
className={`px-3 py-1 rounded-full text-[13px] font-bold shadow ring-1 ring-black/10
|
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 })}
|
{format(new Date(m.demoDate), 'dd.MM.yyyy', { locale: de })}
|
||||||
</span>
|
</span>
|
||||||
@ -495,13 +441,3 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
</div>
|
</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,10 +157,8 @@ export default function EditMatchMetaModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSaved(true)
|
setSaved(true)
|
||||||
|
onSaved?.()
|
||||||
onClose()
|
onClose()
|
||||||
setTimeout(() => {
|
|
||||||
onSaved?.()
|
|
||||||
}, 0)
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[EditMatchMetaModal] save error:', e)
|
console.error('[EditMatchMetaModal] save error:', e)
|
||||||
setError(e?.message || 'Speichern fehlgeschlagen')
|
setError(e?.message || 'Speichern fehlgeschlagen')
|
||||||
@ -169,6 +167,8 @@ export default function EditMatchMetaModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!show) return null
|
||||||
|
|
||||||
// Platzhalter mit aktuellem Namen (falls Options noch laden)
|
// Platzhalter mit aktuellem Namen (falls Options noch laden)
|
||||||
const teamAPlaceholder = loadingTeams ? 'Teams laden …' : (defaultTeamAName || 'Team A wählen …')
|
const teamAPlaceholder = loadingTeams ? 'Teams laden …' : (defaultTeamAName || 'Team A wählen …')
|
||||||
const teamBPlaceholder = loadingTeams ? 'Teams laden …' : (defaultTeamBName || 'Team B wählen …')
|
const teamBPlaceholder = loadingTeams ? 'Teams laden …' : (defaultTeamBName || 'Team B wählen …')
|
||||||
@ -177,7 +177,7 @@ export default function EditMatchMetaModal({
|
|||||||
<Modal
|
<Modal
|
||||||
id="edit-match-meta"
|
id="edit-match-meta"
|
||||||
title="Matchdaten bearbeiten"
|
title="Matchdaten bearbeiten"
|
||||||
show={show}
|
show
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
closeButtonTitle={saved ? '✓ Gespeichert' : saving ? 'Speichern …' : 'Speichern'}
|
closeButtonTitle={saved ? '✓ Gespeichert' : saving ? 'Speichern …' : 'Speichern'}
|
||||||
|
|||||||
@ -149,7 +149,10 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
players: [
|
players: [
|
||||||
|
/* akt. Auswahl für die bearbeitete Seite */
|
||||||
...selected.map(steamId => ({ steamId, teamId: team.id })),
|
...selected.map(steamId => ({ steamId, teamId: team.id })),
|
||||||
|
|
||||||
|
/* unveränderte Gegenseite unbedingt mitschicken! */
|
||||||
...otherInit.map(steamId => ({ steamId, teamId: other.id })),
|
...otherInit.map(steamId => ({ steamId, teamId: other.id })),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@ -162,13 +165,7 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
if (!res.ok) throw new Error()
|
if (!res.ok) throw new Error()
|
||||||
|
|
||||||
setSaved(true)
|
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) {
|
} catch (e) {
|
||||||
console.error('[EditMatchPlayersModal] save error:', e)
|
console.error('[EditMatchPlayersModal] save error:', e)
|
||||||
} finally {
|
} finally {
|
||||||
@ -176,18 +173,18 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ---- Listen trennen ------------------------------------- */
|
/* ---- Listen trennen ------------------------------------- */
|
||||||
const active = players.filter(p => selected.includes(p.steamId))
|
const active = players.filter(p => selected.includes(p.steamId))
|
||||||
const inactive = players.filter(p => !selected.includes(p.steamId))
|
const inactive = players.filter(p => !selected.includes(p.steamId))
|
||||||
|
|
||||||
/* ---- UI -------------------------------------------------- */
|
/* ---- UI -------------------------------------------------- */
|
||||||
|
if (!show) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
id="edit-match-players"
|
id="edit-match-players"
|
||||||
title={`Spieler bearbeiten – ${team.name ?? 'Team'}`}
|
title={`Spieler bearbeiten – ${team.name ?? 'Team'}`}
|
||||||
show={show}
|
show
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
closeButtonTitle={
|
closeButtonTitle={
|
||||||
|
|||||||
@ -1,52 +1,24 @@
|
|||||||
// /app/components/MapVoteBanner.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 { MapVoteState } from '../types/mapvote'
|
import type { MapVoteState } from '../types/mapvote'
|
||||||
|
import { MATCH_EVENTS } from '@/app/lib/sseEvents'
|
||||||
|
|
||||||
type Props = {
|
type Props = { match: any; initialNow: number }
|
||||||
match: any
|
|
||||||
initialNow: number
|
|
||||||
matchBaseTs: number | null
|
|
||||||
sseOpensAtTs?: number | null
|
|
||||||
sseLeadMinutes?: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCountdown(ms: number) {
|
export default function MapVoteBanner({ match, initialNow }: Props) {
|
||||||
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 router = useRouter()
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const { lastEvent } = useSSEStore()
|
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 [state, setState] = useState<MapVoteState | null>(null)
|
||||||
const [error, setError] = useState<string | 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 () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -65,89 +37,32 @@ export default function MapVoteBanner({ match, initialNow, matchBaseTs, sseOpens
|
|||||||
}
|
}
|
||||||
}, [match.id])
|
}, [match.id])
|
||||||
|
|
||||||
const matchDateTs = useMemo(
|
// ✅ tickt NUR im Client, nach Hydration
|
||||||
() => (typeof matchBaseTs === 'number' ? matchBaseTs : null),
|
|
||||||
[matchBaseTs]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = setInterval(() => setNow(Date.now()), 1000)
|
const id = setInterval(() => setNow(Date.now()), 1000)
|
||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// initial
|
|
||||||
useEffect(() => { load() }, [load])
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
// 🔁 Neu laden, wenn Match-Metadaten (z. B. matchDate/bestOf) sich durch refresh ändern
|
// Live-Refresh via SSE
|
||||||
useEffect(() => { load() }, [match.matchDate, match.demoDate, match.bestOf, load])
|
|
||||||
|
|
||||||
// 🔁 Live-Refresh via SSE
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastEvent) return
|
if (!lastEvent) return
|
||||||
const { type } = lastEvent as any
|
// ⬇️ reagiert auf alle match-bezogenen Events: vote-updated, admin-edit, reset, ready, lineup, ...
|
||||||
const evt = (lastEvent as any).payload ?? lastEvent
|
if (!MATCH_EVENTS.has(lastEvent.type)) return
|
||||||
if (evt?.matchId !== match.id) return
|
|
||||||
|
|
||||||
const RELOAD_TYPES = new Set([
|
const matchId = lastEvent.payload?.matchId
|
||||||
'map-vote-updated','map-vote-reset','map-vote-locked','map-vote-unlocked',
|
if (matchId !== match.id) return
|
||||||
'match-updated','match-lineup-updated',
|
|
||||||
])
|
|
||||||
if (!RELOAD_TYPES.has(type)) return
|
|
||||||
|
|
||||||
const rawLead = evt?.leadMinutes
|
load()
|
||||||
const parsedLead = (rawLead !== undefined && rawLead !== null) ? Number(rawLead) : undefined
|
}, [lastEvent, match.id, load])
|
||||||
const nextOpensAtISO =
|
|
||||||
evt?.opensAt
|
|
||||||
? (typeof evt.opensAt === 'string' ? evt.opensAt : new Date(evt.opensAt).toISOString())
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
// sofortige lokale Overrides, ohne auf fetch zu warten
|
// Öffnet 1h vor Match-/Demotermin (stabil, ohne Date.now() im Render)
|
||||||
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(() => {
|
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()
|
if (state?.opensAt) return new Date(state.opensAt).getTime()
|
||||||
// Fallback aus Basis + Lead
|
const base = new Date(match.matchDate ?? match.demoDate ?? initialNow)
|
||||||
if (matchDateTs == null) return new Date(initialNow).getTime()
|
return base.getTime() - 60 * 60 * 1000
|
||||||
const lead = (typeof sseLeadMinutes === 'number')
|
}, [state?.opensAt, match.matchDate, match.demoDate, initialNow])
|
||||||
? 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 isOpen = now >= opensAt
|
||||||
const msToOpen = Math.max(opensAt - now, 0)
|
const msToOpen = Math.max(opensAt - now, 0)
|
||||||
@ -157,6 +72,7 @@ export default function MapVoteBanner({ match, initialNow, matchBaseTs, sseOpens
|
|||||||
? (current.teamId === match.teamA?.id ? match.teamA?.name : match.teamB?.name)
|
? (current.teamId === match.teamA?.id ? match.teamA?.name : match.teamB?.name)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
// ⚠️ leader ist bei dir ein Player-Objekt → .steamId vergleichen
|
||||||
const isLeaderA = !!session?.user?.steamId && match.teamA?.leader?.steamId === session.user.steamId
|
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 isLeaderB = !!session?.user?.steamId && match.teamB?.leader?.steamId === session.user.steamId
|
||||||
const isAdmin = !!session?.user?.isAdmin
|
const isAdmin = !!session?.user?.isAdmin
|
||||||
@ -212,7 +128,7 @@ export default function MapVoteBanner({ match, initialNow, matchBaseTs, sseOpens
|
|||||||
? ' • Auswahl fixiert'
|
? ' • Auswahl fixiert'
|
||||||
: isOpen
|
: isOpen
|
||||||
? (whoIsUp ? ` • am Zug: ${whoIsUp}` : ' • läuft')
|
? (whoIsUp ? ` • am Zug: ${whoIsUp}` : ' • läuft')
|
||||||
: ` • startet ${formatLead(leadMinutes)} vor Matchbeginn`}
|
: ' • startet 1h vor Matchbeginn'}
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-xs text-red-600 dark:text-red-400 mt-0.5">
|
<div className="text-xs text-red-600 dark:text-red-400 mt-0.5">
|
||||||
@ -304,3 +220,13 @@ export default function MapVoteBanner({ match, initialNow, matchBaseTs, sseOpens
|
|||||||
</div>
|
</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,4 +1,3 @@
|
|||||||
// /app/components/MapVotePanel.tsx
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
|
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
|
||||||
@ -36,20 +35,6 @@ const fmtCountdown = (ms: number) => {
|
|||||||
return `${h}:${pad(m)}:${pad(s)}`
|
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 =================== */
|
/* =================== Component =================== */
|
||||||
|
|
||||||
export default function MapVotePanel({ match }: Props) {
|
export default function MapVotePanel({ match }: Props) {
|
||||||
@ -65,13 +50,18 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [adminEditMode, setAdminEditMode] = useState(false)
|
const [adminEditMode, setAdminEditMode] = useState(false)
|
||||||
const [overlayShownOnce, setOverlayShownOnce] = useState(false)
|
const [overlayShownOnce, setOverlayShownOnce] = useState(false)
|
||||||
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null);
|
|
||||||
const [leadOverride, setLeadOverride] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const matchBaseTs = useMemo(() => {
|
/* -------- Timers / open window -------- */
|
||||||
const raw = match.matchDate ?? match.demoDate ?? Date.now();
|
const opensAtTs = useMemo(() => {
|
||||||
return new Date(raw).getTime();
|
const base = new Date(match.matchDate ?? match.demoDate ?? Date.now())
|
||||||
}, [match.matchDate, match.demoDate]);
|
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)
|
||||||
|
}, [])
|
||||||
|
|
||||||
/* -------- Overlay integration -------- */
|
/* -------- Overlay integration -------- */
|
||||||
const overlayIsForThisMatch = overlayData?.matchId === match.id
|
const overlayIsForThisMatch = overlayData?.matchId === match.id
|
||||||
@ -117,115 +107,14 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
}
|
}
|
||||||
}, [match.id])
|
}, [match.id])
|
||||||
|
|
||||||
useEffect(() => { load() }, [load, match.matchDate, match.demoDate])
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpensAtOverride(null)
|
if (!lastEvent) return
|
||||||
}, [match.matchDate, match.demoDate])
|
if (!MATCH_EVENTS.has(lastEvent.type)) return
|
||||||
|
if (lastEvent.payload?.matchId !== match.id) return
|
||||||
// 🔔 SSE: wie in MatchDetails — opensAt/leadMinutes direkt aus dem Event übernehmen
|
load()
|
||||||
useEffect(() => {
|
}, [lastEvent, match.id, load])
|
||||||
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 -------- */
|
/* -------- Admin-Edit Mirror -------- */
|
||||||
const adminEditingBy = state?.adminEdit?.by ?? null
|
const adminEditingBy = state?.adminEdit?.by ?? null
|
||||||
@ -236,6 +125,14 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
}, [adminEditingEnabled, adminEditingBy, session?.user?.steamId])
|
}, [adminEditingEnabled, adminEditingBy, session?.user?.steamId])
|
||||||
|
|
||||||
/* -------- Derived flags & memoized maps -------- */
|
/* -------- 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 me = session?.user
|
||||||
const isAdmin = !!me?.isAdmin
|
const isAdmin = !!me?.isAdmin
|
||||||
const mySteamId = me?.steamId
|
const mySteamId = me?.steamId
|
||||||
@ -580,7 +477,7 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Countdown / Status */}
|
{/* Countdown / Status */}
|
||||||
<div className="mb-4" key={openTs}>
|
<div className="mb-4">
|
||||||
<div className="mx-auto w-full max-w-xl">
|
<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="grid grid-cols-[max-content_1fr_max-content] items-center gap-2">
|
||||||
<div className="w-10 h-10" />
|
<div className="w-10 h-10" />
|
||||||
@ -622,11 +519,8 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<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">
|
||||||
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)}
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
Öffnet in {mounted ? fmtCountdown(msToOpen) : '–:–:–'}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
// /app/components/MatchDetails.tsx
|
/* ────────────────────────────────────────────────────────────────
|
||||||
|
/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)
|
||||||
|
─────────────────────────────────────────────────────────────────*/
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
@ -44,107 +48,6 @@ const adr = (dmg?: number, rounds?: number) =>
|
|||||||
const normalizeMapKey = (raw?: string) =>
|
const normalizeMapKey = (raw?: string) =>
|
||||||
(raw ?? '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
(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 ─────────────────────────────── */
|
/* ─────────────────── Komponente ─────────────────────────────── */
|
||||||
export function MatchDetails({ match, initialNow }: { match: Match; initialNow: number }) {
|
export function MatchDetails({ match, initialNow }: { match: Match; initialNow: number }) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
@ -153,9 +56,6 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
const isAdmin = !!session?.user?.isAdmin
|
const isAdmin = !!session?.user?.isAdmin
|
||||||
const [now, setNow] = useState(initialNow)
|
const [now, setNow] = useState(initialNow)
|
||||||
const [editMetaOpen, setEditMetaOpen] = useState(false)
|
const [editMetaOpen, setEditMetaOpen] = useState(false)
|
||||||
const [opensAtOverride, setOpensAtOverride] = useState<number | null>(null);
|
|
||||||
const [leadOverride, setLeadOverride] = useState<number | null>(null);
|
|
||||||
|
|
||||||
|
|
||||||
/* ─── Rollen & Rechte ─────────────────────────────────────── */
|
/* ─── Rollen & Rechte ─────────────────────────────────────── */
|
||||||
const me = session?.user
|
const me = session?.user
|
||||||
@ -179,13 +79,6 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
const dateString = match.matchDate ?? match.demoDate
|
const dateString = match.matchDate ?? match.demoDate
|
||||||
const readableDate = dateString ? format(new Date(dateString), 'PPpp', { locale: de }) : 'Unbekannt'
|
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 ─── */
|
/* ─── Modal-State: null = geschlossen, 'A' / 'B' = offen ─── */
|
||||||
const [editSide, setEditSide] = useState<EditSide | null>(null)
|
const [editSide, setEditSide] = useState<EditSide | null>(null)
|
||||||
|
|
||||||
@ -195,22 +88,12 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
return () => clearInterval(id)
|
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 voteOpensAtTs = useMemo(() => {
|
||||||
if (opensAtOverride != null) return opensAtOverride; // SSE hat Vorrang
|
const base = match.mapVote?.opensAt
|
||||||
if (match.mapVote?.opensAt) return new Date(match.mapVote.opensAt).getTime(); // vom Server
|
? new Date(match.mapVote.opensAt).getTime()
|
||||||
const lead = (leadOverride != null) ? leadOverride : 60; // kein 60-min Zwang
|
: new Date(match.matchDate ?? match.demoDate ?? initialNow).getTime() - 60 * 60 * 1000
|
||||||
return matchBaseTs - lead * 60_000;
|
return base
|
||||||
}, [opensAtOverride, match.mapVote?.opensAt, matchBaseTs, leadOverride]);
|
}, [match.mapVote?.opensAt, match.matchDate, match.demoDate, initialNow])
|
||||||
|
|
||||||
const sseOpensAtTs = voteOpensAtTs;
|
|
||||||
const sseLeadMinutes = leadOverride;
|
|
||||||
|
|
||||||
const endDate = new Date(voteOpensAtTs)
|
const endDate = new Date(voteOpensAtTs)
|
||||||
const mapvoteStarted = (match.mapVote?.isOpen ?? false) || now >= voteOpensAtTs
|
const mapvoteStarted = (match.mapVote?.isOpen ?? false) || now >= voteOpensAtTs
|
||||||
@ -220,37 +103,19 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
|
|
||||||
/* ─── SSE-Listener ─────────────────────────────────────────── */
|
/* ─── SSE-Listener ─────────────────────────────────────────── */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastEvent) return;
|
if (!lastEvent) return
|
||||||
const evt = (lastEvent as any).payload ?? lastEvent;
|
|
||||||
if (evt?.matchId !== match.id) return;
|
|
||||||
|
|
||||||
if (lastEvent.type === 'map-vote-updated') {
|
// Match gelöscht? → zurück zur Liste
|
||||||
// opensAt aus Event übernehmen
|
if (lastEvent.type === 'match-deleted' && lastEvent.payload?.matchId === match.id) {
|
||||||
if (evt?.opensAt) {
|
router.replace('/schedule')
|
||||||
const ts = typeof evt.opensAt === 'string'
|
return
|
||||||
? 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const REFRESH_TYPES = new Set([
|
// Alle Match-Events → Seite frisch rendern
|
||||||
'map-vote-reset','map-vote-locked','map-vote-unlocked',
|
if (MATCH_EVENTS.has(lastEvent.type) && lastEvent.payload?.matchId === match.id) {
|
||||||
'match-updated','match-lineup-updated',
|
router.refresh()
|
||||||
]);
|
|
||||||
if (REFRESH_TYPES.has(lastEvent.type) && evt?.matchId === match.id) {
|
|
||||||
router.refresh();
|
|
||||||
}
|
}
|
||||||
}, [lastEvent, match.id, router, match.matchDate, match.demoDate, initialNow]);
|
}, [lastEvent, match.id, router])
|
||||||
|
|
||||||
/* ─── Tabellen-Layout (fixe Breiten) ───────────────────────── */
|
/* ─── Tabellen-Layout (fixe Breiten) ───────────────────────── */
|
||||||
const ColGroup = () => (
|
const ColGroup = () => (
|
||||||
@ -416,18 +281,7 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
|
|
||||||
<p className="text-sm text-gray-500">Datum: {readableDate}</p>
|
<p className="text-sm text-gray-500">Datum: {readableDate}</p>
|
||||||
|
|
||||||
{(match.bestOf ?? 1) > 1 && (
|
<div className="text-md">
|
||||||
<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'}
|
<strong>Teams:</strong> {match.teamA?.name ?? 'Unbekannt'} vs. {match.teamB?.name ?? 'Unbekannt'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -435,13 +289,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>
|
||||||
|
|
||||||
<MapVoteBanner
|
<MapVoteBanner match={match} initialNow={initialNow} />
|
||||||
match={match}
|
|
||||||
initialNow={initialNow}
|
|
||||||
matchBaseTs={matchBaseTs}
|
|
||||||
sseOpensAtTs={sseOpensAtTs}
|
|
||||||
sseLeadMinutes={sseLeadMinutes}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* ───────── Team-Blöcke ───────── */}
|
{/* ───────── Team-Blöcke ───────── */}
|
||||||
<div className="border-t pt-4 mt-4 space-y-10">
|
<div className="border-t pt-4 mt-4 space-y-10">
|
||||||
@ -601,7 +449,7 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
defaultDateISO={match.matchDate ?? match.demoDate ?? null}
|
defaultDateISO={match.matchDate ?? match.demoDate ?? null}
|
||||||
defaultMap={match.map ?? null}
|
defaultMap={match.map ?? null}
|
||||||
defaultVoteLeadMinutes={60}
|
defaultVoteLeadMinutes={60}
|
||||||
onSaved={() => { setTimeout(() => router.refresh(), 0) }}
|
onSaved={() => { router.refresh() }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -77,9 +77,6 @@ export default function Modal({
|
|||||||
return () => {
|
return () => {
|
||||||
modalEl.removeEventListener('hsOverlay:close', handleClose)
|
modalEl.removeEventListener('hsOverlay:close', handleClose)
|
||||||
destroyIfExists()
|
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])
|
}, [show, id, onClose])
|
||||||
|
|
||||||
@ -93,10 +90,7 @@ export default function Modal({
|
|||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
if (e.target === e.currentTarget) onClose?.()
|
if (e.target === e.currentTarget) onClose?.()
|
||||||
}}
|
}}
|
||||||
className={
|
className="hs-overlay hidden fixed inset-0 z-80 overflow-y-auto overflow-x-hidden"
|
||||||
"hs-overlay hidden fixed inset-0 z-80 overflow-y-auto overflow-x-hidden " +
|
|
||||||
(show ? "" : "pointer-events-none")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div className="fixed inset-0 -z-10 bg-black/50 dark:bg-neutral-900/70" />
|
<div className="fixed inset-0 -z-10 bg-black/50 dark:bg-neutral-900/70" />
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// /app/lib/sseEvents.ts
|
// sseEvents.ts
|
||||||
|
|
||||||
export const SSE_EVENT_TYPES = [
|
export const SSE_EVENT_TYPES = [
|
||||||
// Kanonisch
|
// Kanonisch
|
||||||
@ -23,7 +23,6 @@ export const SSE_EVENT_TYPES = [
|
|||||||
'expired-sharecode',
|
'expired-sharecode',
|
||||||
'team-invite-revoked',
|
'team-invite-revoked',
|
||||||
'map-vote-updated',
|
'map-vote-updated',
|
||||||
'match-meta-updated',
|
|
||||||
'map-vote-admin-edit',
|
'map-vote-admin-edit',
|
||||||
'match-created',
|
'match-created',
|
||||||
'matches-updated',
|
'matches-updated',
|
||||||
|
|||||||
@ -41,11 +41,10 @@ export type MapVoteState = {
|
|||||||
currentIndex: number
|
currentIndex: number
|
||||||
locked: boolean
|
locked: boolean
|
||||||
opensAt: string | null
|
opensAt: string | null
|
||||||
leadMinutes?: number | null
|
|
||||||
steps: MapVoteStep[]
|
steps: MapVoteStep[]
|
||||||
teams?: {
|
teams?: {
|
||||||
teamA: MapVoteTeam
|
teamA: MapVoteTeam
|
||||||
teamB: MapVoteTeam
|
teamB: MapVoteTeam
|
||||||
}
|
}
|
||||||
adminEdit?: MapVoteAdminEdit
|
adminEdit?: MapVoteAdminEdit // ⬅️ NEU
|
||||||
}
|
}
|
||||||
@ -2,15 +2,6 @@
|
|||||||
|
|
||||||
import { Player, Team } from './team'
|
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 = {
|
export type Match = {
|
||||||
/* Basis-Infos ---------------------------------------------------- */
|
/* Basis-Infos ---------------------------------------------------- */
|
||||||
id : string
|
id : string
|
||||||
@ -31,11 +22,9 @@ export type Match = {
|
|||||||
teamB: Team
|
teamB: Team
|
||||||
|
|
||||||
mapVote?: {
|
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
|
||||||
locked?: boolean | null
|
|
||||||
steps? : MapVoteStep[]
|
|
||||||
} | null
|
} | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -286,7 +286,6 @@ exports.Prisma.MapVoteScalarFieldEnum = {
|
|||||||
currentIdx: 'currentIdx',
|
currentIdx: 'currentIdx',
|
||||||
locked: 'locked',
|
locked: 'locked',
|
||||||
opensAt: 'opensAt',
|
opensAt: 'opensAt',
|
||||||
leadMinutes: 'leadMinutes',
|
|
||||||
adminEditingBy: 'adminEditingBy',
|
adminEditingBy: 'adminEditingBy',
|
||||||
adminEditingSince: 'adminEditingSince',
|
adminEditingSince: 'adminEditingSince',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
|
|||||||
45
src/generated/prisma/index.d.ts
vendored
45
src/generated/prisma/index.d.ts
vendored
@ -16185,13 +16185,11 @@ export namespace Prisma {
|
|||||||
export type MapVoteAvgAggregateOutputType = {
|
export type MapVoteAvgAggregateOutputType = {
|
||||||
bestOf: number | null
|
bestOf: number | null
|
||||||
currentIdx: number | null
|
currentIdx: number | null
|
||||||
leadMinutes: number | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MapVoteSumAggregateOutputType = {
|
export type MapVoteSumAggregateOutputType = {
|
||||||
bestOf: number | null
|
bestOf: number | null
|
||||||
currentIdx: number | null
|
currentIdx: number | null
|
||||||
leadMinutes: number | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MapVoteMinAggregateOutputType = {
|
export type MapVoteMinAggregateOutputType = {
|
||||||
@ -16201,7 +16199,6 @@ export namespace Prisma {
|
|||||||
currentIdx: number | null
|
currentIdx: number | null
|
||||||
locked: boolean | null
|
locked: boolean | null
|
||||||
opensAt: Date | null
|
opensAt: Date | null
|
||||||
leadMinutes: number | null
|
|
||||||
adminEditingBy: string | null
|
adminEditingBy: string | null
|
||||||
adminEditingSince: Date | null
|
adminEditingSince: Date | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
@ -16215,7 +16212,6 @@ export namespace Prisma {
|
|||||||
currentIdx: number | null
|
currentIdx: number | null
|
||||||
locked: boolean | null
|
locked: boolean | null
|
||||||
opensAt: Date | null
|
opensAt: Date | null
|
||||||
leadMinutes: number | null
|
|
||||||
adminEditingBy: string | null
|
adminEditingBy: string | null
|
||||||
adminEditingSince: Date | null
|
adminEditingSince: Date | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
@ -16230,7 +16226,6 @@ export namespace Prisma {
|
|||||||
currentIdx: number
|
currentIdx: number
|
||||||
locked: number
|
locked: number
|
||||||
opensAt: number
|
opensAt: number
|
||||||
leadMinutes: number
|
|
||||||
adminEditingBy: number
|
adminEditingBy: number
|
||||||
adminEditingSince: number
|
adminEditingSince: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
@ -16242,13 +16237,11 @@ export namespace Prisma {
|
|||||||
export type MapVoteAvgAggregateInputType = {
|
export type MapVoteAvgAggregateInputType = {
|
||||||
bestOf?: true
|
bestOf?: true
|
||||||
currentIdx?: true
|
currentIdx?: true
|
||||||
leadMinutes?: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MapVoteSumAggregateInputType = {
|
export type MapVoteSumAggregateInputType = {
|
||||||
bestOf?: true
|
bestOf?: true
|
||||||
currentIdx?: true
|
currentIdx?: true
|
||||||
leadMinutes?: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MapVoteMinAggregateInputType = {
|
export type MapVoteMinAggregateInputType = {
|
||||||
@ -16258,7 +16251,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: true
|
currentIdx?: true
|
||||||
locked?: true
|
locked?: true
|
||||||
opensAt?: true
|
opensAt?: true
|
||||||
leadMinutes?: true
|
|
||||||
adminEditingBy?: true
|
adminEditingBy?: true
|
||||||
adminEditingSince?: true
|
adminEditingSince?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
@ -16272,7 +16264,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: true
|
currentIdx?: true
|
||||||
locked?: true
|
locked?: true
|
||||||
opensAt?: true
|
opensAt?: true
|
||||||
leadMinutes?: true
|
|
||||||
adminEditingBy?: true
|
adminEditingBy?: true
|
||||||
adminEditingSince?: true
|
adminEditingSince?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
@ -16287,7 +16278,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: true
|
currentIdx?: true
|
||||||
locked?: true
|
locked?: true
|
||||||
opensAt?: true
|
opensAt?: true
|
||||||
leadMinutes?: true
|
|
||||||
adminEditingBy?: true
|
adminEditingBy?: true
|
||||||
adminEditingSince?: true
|
adminEditingSince?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
@ -16389,7 +16379,6 @@ export namespace Prisma {
|
|||||||
currentIdx: number
|
currentIdx: number
|
||||||
locked: boolean
|
locked: boolean
|
||||||
opensAt: Date | null
|
opensAt: Date | null
|
||||||
leadMinutes: number
|
|
||||||
adminEditingBy: string | null
|
adminEditingBy: string | null
|
||||||
adminEditingSince: Date | null
|
adminEditingSince: Date | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@ -16423,7 +16412,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: boolean
|
currentIdx?: boolean
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: boolean
|
opensAt?: boolean
|
||||||
leadMinutes?: boolean
|
|
||||||
adminEditingBy?: boolean
|
adminEditingBy?: boolean
|
||||||
adminEditingSince?: boolean
|
adminEditingSince?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
@ -16441,7 +16429,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: boolean
|
currentIdx?: boolean
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: boolean
|
opensAt?: boolean
|
||||||
leadMinutes?: boolean
|
|
||||||
adminEditingBy?: boolean
|
adminEditingBy?: boolean
|
||||||
adminEditingSince?: boolean
|
adminEditingSince?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
@ -16457,7 +16444,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: boolean
|
currentIdx?: boolean
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: boolean
|
opensAt?: boolean
|
||||||
leadMinutes?: boolean
|
|
||||||
adminEditingBy?: boolean
|
adminEditingBy?: boolean
|
||||||
adminEditingSince?: boolean
|
adminEditingSince?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
@ -16473,14 +16459,13 @@ export namespace Prisma {
|
|||||||
currentIdx?: boolean
|
currentIdx?: boolean
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: boolean
|
opensAt?: boolean
|
||||||
leadMinutes?: boolean
|
|
||||||
adminEditingBy?: boolean
|
adminEditingBy?: boolean
|
||||||
adminEditingSince?: boolean
|
adminEditingSince?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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 MapVoteInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
|
export type MapVoteInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
|
||||||
match?: boolean | MatchDefaultArgs<ExtArgs>
|
match?: boolean | MatchDefaultArgs<ExtArgs>
|
||||||
steps?: boolean | MapVote$stepsArgs<ExtArgs>
|
steps?: boolean | MapVote$stepsArgs<ExtArgs>
|
||||||
@ -16507,7 +16492,6 @@ export namespace Prisma {
|
|||||||
currentIdx: number
|
currentIdx: number
|
||||||
locked: boolean
|
locked: boolean
|
||||||
opensAt: Date | null
|
opensAt: Date | null
|
||||||
leadMinutes: number
|
|
||||||
adminEditingBy: string | null
|
adminEditingBy: string | null
|
||||||
adminEditingSince: Date | null
|
adminEditingSince: Date | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@ -16944,7 +16928,6 @@ export namespace Prisma {
|
|||||||
readonly currentIdx: FieldRef<"MapVote", 'Int'>
|
readonly currentIdx: FieldRef<"MapVote", 'Int'>
|
||||||
readonly locked: FieldRef<"MapVote", 'Boolean'>
|
readonly locked: FieldRef<"MapVote", 'Boolean'>
|
||||||
readonly opensAt: FieldRef<"MapVote", 'DateTime'>
|
readonly opensAt: FieldRef<"MapVote", 'DateTime'>
|
||||||
readonly leadMinutes: FieldRef<"MapVote", 'Int'>
|
|
||||||
readonly adminEditingBy: FieldRef<"MapVote", 'String'>
|
readonly adminEditingBy: FieldRef<"MapVote", 'String'>
|
||||||
readonly adminEditingSince: FieldRef<"MapVote", 'DateTime'>
|
readonly adminEditingSince: FieldRef<"MapVote", 'DateTime'>
|
||||||
readonly createdAt: FieldRef<"MapVote", 'DateTime'>
|
readonly createdAt: FieldRef<"MapVote", 'DateTime'>
|
||||||
@ -18785,7 +18768,6 @@ export namespace Prisma {
|
|||||||
currentIdx: 'currentIdx',
|
currentIdx: 'currentIdx',
|
||||||
locked: 'locked',
|
locked: 'locked',
|
||||||
opensAt: 'opensAt',
|
opensAt: 'opensAt',
|
||||||
leadMinutes: 'leadMinutes',
|
|
||||||
adminEditingBy: 'adminEditingBy',
|
adminEditingBy: 'adminEditingBy',
|
||||||
adminEditingSince: 'adminEditingSince',
|
adminEditingSince: 'adminEditingSince',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
@ -20084,7 +20066,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFilter<"MapVote"> | number
|
currentIdx?: IntFilter<"MapVote"> | number
|
||||||
locked?: BoolFilter<"MapVote"> | boolean
|
locked?: BoolFilter<"MapVote"> | boolean
|
||||||
opensAt?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
opensAt?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
||||||
leadMinutes?: IntFilter<"MapVote"> | number
|
|
||||||
adminEditingBy?: StringNullableFilter<"MapVote"> | string | null
|
adminEditingBy?: StringNullableFilter<"MapVote"> | string | null
|
||||||
adminEditingSince?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
adminEditingSince?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
||||||
createdAt?: DateTimeFilter<"MapVote"> | Date | string
|
createdAt?: DateTimeFilter<"MapVote"> | Date | string
|
||||||
@ -20101,7 +20082,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: SortOrder
|
currentIdx?: SortOrder
|
||||||
locked?: SortOrder
|
locked?: SortOrder
|
||||||
opensAt?: SortOrderInput | SortOrder
|
opensAt?: SortOrderInput | SortOrder
|
||||||
leadMinutes?: SortOrder
|
|
||||||
adminEditingBy?: SortOrderInput | SortOrder
|
adminEditingBy?: SortOrderInput | SortOrder
|
||||||
adminEditingSince?: SortOrderInput | SortOrder
|
adminEditingSince?: SortOrderInput | SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
@ -20121,7 +20101,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFilter<"MapVote"> | number
|
currentIdx?: IntFilter<"MapVote"> | number
|
||||||
locked?: BoolFilter<"MapVote"> | boolean
|
locked?: BoolFilter<"MapVote"> | boolean
|
||||||
opensAt?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
opensAt?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
||||||
leadMinutes?: IntFilter<"MapVote"> | number
|
|
||||||
adminEditingBy?: StringNullableFilter<"MapVote"> | string | null
|
adminEditingBy?: StringNullableFilter<"MapVote"> | string | null
|
||||||
adminEditingSince?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
adminEditingSince?: DateTimeNullableFilter<"MapVote"> | Date | string | null
|
||||||
createdAt?: DateTimeFilter<"MapVote"> | Date | string
|
createdAt?: DateTimeFilter<"MapVote"> | Date | string
|
||||||
@ -20138,7 +20117,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: SortOrder
|
currentIdx?: SortOrder
|
||||||
locked?: SortOrder
|
locked?: SortOrder
|
||||||
opensAt?: SortOrderInput | SortOrder
|
opensAt?: SortOrderInput | SortOrder
|
||||||
leadMinutes?: SortOrder
|
|
||||||
adminEditingBy?: SortOrderInput | SortOrder
|
adminEditingBy?: SortOrderInput | SortOrder
|
||||||
adminEditingSince?: SortOrderInput | SortOrder
|
adminEditingSince?: SortOrderInput | SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
@ -20161,7 +20139,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntWithAggregatesFilter<"MapVote"> | number
|
currentIdx?: IntWithAggregatesFilter<"MapVote"> | number
|
||||||
locked?: BoolWithAggregatesFilter<"MapVote"> | boolean
|
locked?: BoolWithAggregatesFilter<"MapVote"> | boolean
|
||||||
opensAt?: DateTimeNullableWithAggregatesFilter<"MapVote"> | Date | string | null
|
opensAt?: DateTimeNullableWithAggregatesFilter<"MapVote"> | Date | string | null
|
||||||
leadMinutes?: IntWithAggregatesFilter<"MapVote"> | number
|
|
||||||
adminEditingBy?: StringNullableWithAggregatesFilter<"MapVote"> | string | null
|
adminEditingBy?: StringNullableWithAggregatesFilter<"MapVote"> | string | null
|
||||||
adminEditingSince?: DateTimeNullableWithAggregatesFilter<"MapVote"> | Date | string | null
|
adminEditingSince?: DateTimeNullableWithAggregatesFilter<"MapVote"> | Date | string | null
|
||||||
createdAt?: DateTimeWithAggregatesFilter<"MapVote"> | Date | string
|
createdAt?: DateTimeWithAggregatesFilter<"MapVote"> | Date | string
|
||||||
@ -21448,7 +21425,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: number
|
currentIdx?: number
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: Date | string | null
|
opensAt?: Date | string | null
|
||||||
leadMinutes?: number
|
|
||||||
adminEditingBy?: string | null
|
adminEditingBy?: string | null
|
||||||
adminEditingSince?: Date | string | null
|
adminEditingSince?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@ -21465,7 +21441,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: number
|
currentIdx?: number
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: Date | string | null
|
opensAt?: Date | string | null
|
||||||
leadMinutes?: number
|
|
||||||
adminEditingBy?: string | null
|
adminEditingBy?: string | null
|
||||||
adminEditingSince?: Date | string | null
|
adminEditingSince?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@ -21480,7 +21455,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
|
||||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@ -21497,7 +21471,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
|
||||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@ -21513,7 +21486,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: number
|
currentIdx?: number
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: Date | string | null
|
opensAt?: Date | string | null
|
||||||
leadMinutes?: number
|
|
||||||
adminEditingBy?: string | null
|
adminEditingBy?: string | null
|
||||||
adminEditingSince?: Date | string | null
|
adminEditingSince?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@ -21527,7 +21499,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
|
||||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@ -21542,7 +21513,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
|
||||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@ -22708,7 +22678,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: SortOrder
|
currentIdx?: SortOrder
|
||||||
locked?: SortOrder
|
locked?: SortOrder
|
||||||
opensAt?: SortOrder
|
opensAt?: SortOrder
|
||||||
leadMinutes?: SortOrder
|
|
||||||
adminEditingBy?: SortOrder
|
adminEditingBy?: SortOrder
|
||||||
adminEditingSince?: SortOrder
|
adminEditingSince?: SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
@ -22718,7 +22687,6 @@ export namespace Prisma {
|
|||||||
export type MapVoteAvgOrderByAggregateInput = {
|
export type MapVoteAvgOrderByAggregateInput = {
|
||||||
bestOf?: SortOrder
|
bestOf?: SortOrder
|
||||||
currentIdx?: SortOrder
|
currentIdx?: SortOrder
|
||||||
leadMinutes?: SortOrder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MapVoteMaxOrderByAggregateInput = {
|
export type MapVoteMaxOrderByAggregateInput = {
|
||||||
@ -22728,7 +22696,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: SortOrder
|
currentIdx?: SortOrder
|
||||||
locked?: SortOrder
|
locked?: SortOrder
|
||||||
opensAt?: SortOrder
|
opensAt?: SortOrder
|
||||||
leadMinutes?: SortOrder
|
|
||||||
adminEditingBy?: SortOrder
|
adminEditingBy?: SortOrder
|
||||||
adminEditingSince?: SortOrder
|
adminEditingSince?: SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
@ -22742,7 +22709,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: SortOrder
|
currentIdx?: SortOrder
|
||||||
locked?: SortOrder
|
locked?: SortOrder
|
||||||
opensAt?: SortOrder
|
opensAt?: SortOrder
|
||||||
leadMinutes?: SortOrder
|
|
||||||
adminEditingBy?: SortOrder
|
adminEditingBy?: SortOrder
|
||||||
adminEditingSince?: SortOrder
|
adminEditingSince?: SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
@ -22752,7 +22718,6 @@ export namespace Prisma {
|
|||||||
export type MapVoteSumOrderByAggregateInput = {
|
export type MapVoteSumOrderByAggregateInput = {
|
||||||
bestOf?: SortOrder
|
bestOf?: SortOrder
|
||||||
currentIdx?: SortOrder
|
currentIdx?: SortOrder
|
||||||
leadMinutes?: SortOrder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EnumMapVoteActionFilter<$PrismaModel = never> = {
|
export type EnumMapVoteActionFilter<$PrismaModel = never> = {
|
||||||
@ -26881,7 +26846,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: number
|
currentIdx?: number
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: Date | string | null
|
opensAt?: Date | string | null
|
||||||
leadMinutes?: number
|
|
||||||
adminEditingBy?: string | null
|
adminEditingBy?: string | null
|
||||||
adminEditingSince?: Date | string | null
|
adminEditingSince?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@ -26896,7 +26860,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: number
|
currentIdx?: number
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: Date | string | null
|
opensAt?: Date | string | null
|
||||||
leadMinutes?: number
|
|
||||||
adminEditingBy?: string | null
|
adminEditingBy?: string | null
|
||||||
adminEditingSince?: Date | string | null
|
adminEditingSince?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@ -27145,7 +27108,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
|
||||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@ -27160,7 +27122,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
|
||||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@ -29188,7 +29149,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: number
|
currentIdx?: number
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: Date | string | null
|
opensAt?: Date | string | null
|
||||||
leadMinutes?: number
|
|
||||||
adminEditingBy?: string | null
|
adminEditingBy?: string | null
|
||||||
adminEditingSince?: Date | string | null
|
adminEditingSince?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@ -29204,7 +29164,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: number
|
currentIdx?: number
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
opensAt?: Date | string | null
|
opensAt?: Date | string | null
|
||||||
leadMinutes?: number
|
|
||||||
adminEditingBy?: string | null
|
adminEditingBy?: string | null
|
||||||
adminEditingSince?: Date | string | null
|
adminEditingSince?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
@ -29344,7 +29303,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
|
||||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@ -29360,7 +29318,6 @@ export namespace Prisma {
|
|||||||
currentIdx?: IntFieldUpdateOperationsInput | number
|
currentIdx?: IntFieldUpdateOperationsInput | number
|
||||||
locked?: BoolFieldUpdateOperationsInput | boolean
|
locked?: BoolFieldUpdateOperationsInput | boolean
|
||||||
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
opensAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
leadMinutes?: IntFieldUpdateOperationsInput | number
|
|
||||||
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
adminEditingBy?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
adminEditingSince?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-da0847ed2b650f4e980bae112054fc21d3b01519299b5975e3d05dad9fd53b68",
|
"name": "prisma-client-6d46bb441ad6771c9d4c18422a89623b795f3a4d205bdd0a7fbf78b6d9d34ce6",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "index-browser.js",
|
"browser": "index-browser.js",
|
||||||
|
|||||||
@ -323,8 +323,6 @@ model MapVote {
|
|||||||
locked Boolean @default(false)
|
locked Boolean @default(false)
|
||||||
opensAt DateTime?
|
opensAt DateTime?
|
||||||
|
|
||||||
leadMinutes Int @default(60)
|
|
||||||
|
|
||||||
adminEditingBy String?
|
adminEditingBy String?
|
||||||
adminEditingSince DateTime?
|
adminEditingSince DateTime?
|
||||||
|
|
||||||
|
|||||||
@ -286,7 +286,6 @@ exports.Prisma.MapVoteScalarFieldEnum = {
|
|||||||
currentIdx: 'currentIdx',
|
currentIdx: 'currentIdx',
|
||||||
locked: 'locked',
|
locked: 'locked',
|
||||||
opensAt: 'opensAt',
|
opensAt: 'opensAt',
|
||||||
leadMinutes: 'leadMinutes',
|
|
||||||
adminEditingBy: 'adminEditingBy',
|
adminEditingBy: 'adminEditingBy',
|
||||||
adminEditingSince: 'adminEditingSince',
|
adminEditingSince: 'adminEditingSince',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user