updated mapvote
This commit is contained in:
parent
ba69e99120
commit
c4c714e5ca
109
package-lock.json
generated
109
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.16.3",
|
"@prisma/client": "^6.17.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",
|
||||||
@ -60,7 +60,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.16.3",
|
"prisma": "^6.17.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",
|
||||||
@ -85,6 +85,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
},
|
},
|
||||||
@ -122,7 +123,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/accessibility": "^3.1.1",
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@ -1577,6 +1577,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||||
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
}
|
}
|
||||||
@ -1597,9 +1598,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.16.3",
|
"version": "6.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.0.tgz",
|
||||||
"integrity": "sha512-JfNfAtXG+/lIopsvoZlZiH2k5yNx87mcTS4t9/S5oufM1nKdXYxOvpDC1XoTCFBa5cQh7uXnbMPsmZrwZY80xw==",
|
"integrity": "sha512-b42mTLOdLEZ6e/igu8CLdccAUX9AwHknQQ1+pHOftnzDP2QoyZyFvcANqSLs5ockimFKJnV7Ljf+qrhNYf6oAg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -1619,9 +1620,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/config": {
|
"node_modules/@prisma/config": {
|
||||||
"version": "6.16.3",
|
"version": "6.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.0.tgz",
|
||||||
"integrity": "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ==",
|
"integrity": "sha512-k8tuChKpkO/Vj7ZEzaQMNflNGbaW4X0r8+PC+W2JaqVRdiS2+ORSv1SrDwNxsb8YyzIQJucXqLGZbgxD97ZhsQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1632,53 +1633,53 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/debug": {
|
"node_modules/@prisma/debug": {
|
||||||
"version": "6.16.3",
|
"version": "6.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.0.tgz",
|
||||||
"integrity": "sha512-89DdqWtdKd7qoc9/qJCKLTazj3W3zPEiz0hc7HfZdpjzm21c7orOUB5oHWJsG+4KbV4cWU5pefq3CuDVYF9vgA==",
|
"integrity": "sha512-eE2CB32nr1hRqyLVnOAVY6c//iSJ/PN+Yfoa/2sEzLGpORaCg61d+nvdAkYSh+6Y2B8L4BVyzkRMANLD6nnC2g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.16.3",
|
"version": "6.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.0.tgz",
|
||||||
"integrity": "sha512-b+Rl4nzQDcoqe6RIpSHv8f5lLnwdDGvXhHjGDiokObguAAv/O1KaX1Oc69mBW/GFWKQpCkOraobLjU6s1h8HGg==",
|
"integrity": "sha512-XhE9v3hDQTNgCYMjogcCYKi7HCEkZf9WwTGuXy8cmY8JUijvU0ap4M7pGLx4pBblkp5EwUsYzw1YLtH7yi0GZw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.16.3",
|
"@prisma/debug": "6.17.0",
|
||||||
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
"@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||||
"@prisma/fetch-engine": "6.16.3",
|
"@prisma/fetch-engine": "6.17.0",
|
||||||
"@prisma/get-platform": "6.16.3"
|
"@prisma/get-platform": "6.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
"version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a.tgz",
|
||||||
"integrity": "sha512-fftRmosBex48Ph1v2ll1FrPpirwtPZpNkE5CDCY1Lw2SD2ctyrLlVlHiuxDAAlALwWBOkPbAll4+EaqdGuMhJw==",
|
"integrity": "sha512-G0VU4uFDreATgTz4sh3dTtU2C+jn+J6c060ixavWZaUaSRZsNQhSPW26lbfez7GHzR02RGCdqs5UcSuGBC3yLw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.16.3",
|
"version": "6.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.0.tgz",
|
||||||
"integrity": "sha512-bUoRIkVaI+CCaVGrSfcKev0/Mk4ateubqWqGZvQ9uCqFv2ENwWIR3OeNuGin96nZn5+SkebcD7RGgKr/+mJelw==",
|
"integrity": "sha512-YSl5R3WIAPrmshYPkaaszOsBIWRAovOCHn3y7gkTNGG51LjYW4pi6PFNkGouW6CA06qeTjTbGrDRCgFjnmVWDg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.16.3",
|
"@prisma/debug": "6.17.0",
|
||||||
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
"@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||||
"@prisma/get-platform": "6.16.3"
|
"@prisma/get-platform": "6.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "6.16.3",
|
"version": "6.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.0.tgz",
|
||||||
"integrity": "sha512-X1LxiFXinJ4iQehrodGp0f66Dv6cDL0GbRlcCoLtSu6f4Wi+hgo7eND/afIs5029GQLgNWKZ46vn8hjyXTsHLA==",
|
"integrity": "sha512-3tEKChrnlmLXPd870oiVfRvj7vVKuxqP349hYaMDsbV4TZd3+lFqw8KTI2Tbq5DopamfNuNqhVCj+R6ZxKKYGQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.16.3"
|
"@prisma/debug": "6.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
@ -2067,7 +2068,6 @@
|
|||||||
"integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==",
|
"integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
@ -2085,7 +2085,6 @@
|
|||||||
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
|
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@ -2183,7 +2182,6 @@
|
|||||||
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
|
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.30.1",
|
"@typescript-eslint/scope-manager": "8.30.1",
|
||||||
"@typescript-eslint/types": "8.30.1",
|
"@typescript-eslint/types": "8.30.1",
|
||||||
@ -2617,7 +2615,6 @@
|
|||||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -3286,6 +3283,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -3426,7 +3424,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
@ -3883,7 +3880,6 @@
|
|||||||
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
|
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@ -4058,7 +4054,6 @@
|
|||||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.8",
|
"array-includes": "^3.1.8",
|
||||||
@ -5407,6 +5402,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
}
|
}
|
||||||
@ -5835,6 +5831,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
},
|
},
|
||||||
@ -6022,7 +6019,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-15.3.0.tgz",
|
||||||
"integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==",
|
"integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "15.3.0",
|
"@next/env": "15.3.0",
|
||||||
"@swc/counter": "0.1.3",
|
"@swc/counter": "0.1.3",
|
||||||
@ -6315,7 +6311,8 @@
|
|||||||
"version": "0.9.15",
|
"version": "0.9.15",
|
||||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||||
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@ -6332,6 +6329,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||||
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
@ -6460,6 +6458,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
|
||||||
"integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
|
"integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10.13.0 || >=12.0.0"
|
"node": "^10.13.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
@ -6746,6 +6745,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
|
||||||
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pretty-format": "^3.8.0"
|
"pretty-format": "^3.8.0"
|
||||||
},
|
},
|
||||||
@ -6776,19 +6776,19 @@
|
|||||||
"version": "3.8.0",
|
"version": "3.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "6.16.3",
|
"version": "6.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.0.tgz",
|
||||||
"integrity": "sha512-4tJq3KB9WRshH5+QmzOLV54YMkNlKOtLKaSdvraI5kC/axF47HuOw6zDM8xrxJ6s9o2WodY654On4XKkrobQdQ==",
|
"integrity": "sha512-rcvldz98r+2bVCs0MldQCBaaVJRCj9Ew4IqphLATF89OJcSzwRQpwnKXR+W2+2VjK7/o2x3ffu5+2N3Muu6Dbw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "6.16.3",
|
"@prisma/config": "6.17.0",
|
||||||
"@prisma/engines": "6.16.3"
|
"@prisma/engines": "6.17.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js"
|
"prisma": "build/index.js"
|
||||||
@ -6902,7 +6902,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -6912,7 +6911,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@ -6982,7 +6980,8 @@
|
|||||||
"version": "0.14.1",
|
"version": "0.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
@ -7639,8 +7638,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
||||||
"integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==",
|
"integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
@ -7697,7 +7695,6 @@
|
|||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -7923,7 +7920,6 @@
|
|||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -8179,7 +8175,8 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"license": "ISC"
|
"license": "ISC",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/yn": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
|
|||||||
@ -21,7 +21,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.16.3",
|
"@prisma/client": "^6.17.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",
|
||||||
@ -66,7 +66,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.16.3",
|
"prisma": "^6.17.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",
|
||||||
|
|||||||
@ -11,7 +11,6 @@ export default function AdminPage() {
|
|||||||
const activeTab: 'teams' | 'matches' | 'privacy' | '' =
|
const activeTab: 'teams' | 'matches' | 'privacy' | '' =
|
||||||
pathname.startsWith('/admin/teams') ? 'teams' :
|
pathname.startsWith('/admin/teams') ? 'teams' :
|
||||||
pathname.startsWith('/admin/matches') ? 'matches' :
|
pathname.startsWith('/admin/matches') ? 'matches' :
|
||||||
pathname.startsWith('/admin/privacy') ? 'privacy' :
|
|
||||||
''
|
''
|
||||||
|
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
@ -22,12 +21,6 @@ export default function AdminPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|
||||||
case 'privacy':
|
|
||||||
return (
|
|
||||||
<Card title="Datenschutz"
|
|
||||||
description="Einstellungen zum Schutz deiner Daten." />
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'teams':
|
case 'teams':
|
||||||
return (
|
return (
|
||||||
<Card title="Teams"
|
<Card title="Teams"
|
||||||
|
|||||||
@ -6,7 +6,6 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
|||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab name="Spielpläne" href="/admin/matches" />
|
<Tab name="Spielpläne" href="/admin/matches" />
|
||||||
<Tab name="Privacy" href="/admin/privacy" />
|
|
||||||
<Tab name="Teams" href="/admin/teams" />
|
<Tab name="Teams" href="/admin/teams" />
|
||||||
<Tab name="Serververwaltung" href="/admin/server" />
|
<Tab name="Serververwaltung" href="/admin/server" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
// src/app/components/GameBanner.tsx
|
// src/app/[locale]/components/GameBanner.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, {useEffect, useRef, useState} from 'react'
|
import React, {useEffect, useRef, useState} from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import {useUiChromeStore} from '@/lib/useUiChromeStore'
|
import { useGameBannerStore } from '@/lib/useGameBannerStore'
|
||||||
import {MAP_OPTIONS} from '@/lib/mapOptions'
|
import {MAP_OPTIONS} from '@/lib/mapOptions'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
@ -26,6 +26,8 @@ type Props = {
|
|||||||
inline?: boolean
|
inline?: boolean
|
||||||
connectUri?: string
|
connectUri?: string
|
||||||
missingCount?: number
|
missingCount?: number
|
||||||
|
bgUrl?: string
|
||||||
|
iconUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- helpers ---------- */
|
/* ---------- helpers ---------- */
|
||||||
@ -70,7 +72,7 @@ export default function GameBanner(props: Props) {
|
|||||||
} = props
|
} = props
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
const setBannerPx = useUiChromeStore(s => s.setGameBannerPx)
|
const setBannerPx = useGameBannerStore(s => s.setGameBannerPx)
|
||||||
const isSmDown = useIsSmDown()
|
const isSmDown = useIsSmDown()
|
||||||
const tGameBanner = useTranslations('game-banner')
|
const tGameBanner = useTranslations('game-banner')
|
||||||
|
|
||||||
@ -125,8 +127,9 @@ export default function GameBanner(props: Props) {
|
|||||||
? 'bg-emerald-700/95 text-white ring-1 ring-black/10'
|
? 'bg-emerald-700/95 text-white ring-1 ring-black/10'
|
||||||
: 'bg-amber-700/95 text-white ring-1 ring-black/10'
|
: 'bg-amber-700/95 text-white ring-1 ring-black/10'
|
||||||
|
|
||||||
const bgUrl = pickMapImageFromOptions(mapKey)
|
const bgUrl = props.bgUrl ?? pickMapImageFromOptions(mapKey)
|
||||||
const iconUrl = isConnected ? (pickMapIcon(mapKey) ?? '') : '/assets/img/icons/ui/disconnect.svg'
|
const iconUrl = props.iconUrl ?? (isConnected ? (pickMapIcon(mapKey) ?? '') : '/assets/img/icons/ui/disconnect.svg')
|
||||||
|
|
||||||
|
|
||||||
const pretty = {
|
const pretty = {
|
||||||
map: mapLabel ?? mapKey ?? '—',
|
map: mapLabel ?? mapKey ?? '—',
|
||||||
|
|||||||
35
src/app/[locale]/components/GameBannerHost.tsx
Normal file
35
src/app/[locale]/components/GameBannerHost.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// /src/app/[locale]/components/GameBannerHost.tsx
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
import GameBanner from './GameBanner'
|
||||||
|
import { useGameBannerStore } from '@/lib/useGameBannerStore'
|
||||||
|
|
||||||
|
export default function GameBannerHost() {
|
||||||
|
const banner = useGameBannerStore(s => s.gameBanner)
|
||||||
|
const setBanner = useGameBannerStore(s => s.setGameBanner)
|
||||||
|
const patch = useGameBannerStore(s => s.patchGameBanner)
|
||||||
|
|
||||||
|
if (!banner || !banner.visible) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GameBanner
|
||||||
|
variant={banner.variant ?? 'disconnected'}
|
||||||
|
visible={true}
|
||||||
|
zIndex={banner.zIndex ?? 9999}
|
||||||
|
inline={banner.inline ?? false}
|
||||||
|
serverLabel={banner.serverLabel}
|
||||||
|
mapKey={banner.mapKey}
|
||||||
|
mapLabel={banner.mapLabel}
|
||||||
|
bgUrl={banner.bgUrl}
|
||||||
|
iconUrl={banner.iconUrl}
|
||||||
|
connectUri={banner.connectUri}
|
||||||
|
phase={banner.phase ?? 'lobby'}
|
||||||
|
score={banner.score ?? '– : –'}
|
||||||
|
connectedCount={banner.connectedCount ?? 0}
|
||||||
|
totalExpected={banner.totalExpected ?? 10}
|
||||||
|
missingCount={banner.missingCount ?? 0}
|
||||||
|
onReconnect={() => patch({ variant: 'connected' })}
|
||||||
|
onDisconnect={() => setBanner(null)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
// src/app/components/GameBannerSpacer.tsx
|
// /src/app/[locale]/components/GameBannerSpacer.tsx
|
||||||
'use client'
|
'use client'
|
||||||
import {useUiChromeStore} from '@/lib/useUiChromeStore'
|
import { useGameBannerStore } from '@/lib/useGameBannerStore'
|
||||||
|
|
||||||
export default function GameBannerSpacer({ className = '' }: { className?: string }) {
|
export default function GameBannerSpacer({ className = '' }: { className?: string }) {
|
||||||
const bannerPx = useUiChromeStore(s => s.gameBannerPx)
|
const bannerPx = useGameBannerStore(s => s.gameBannerPx)
|
||||||
return <div className={className} style={{ height: bannerPx ?? 0 }} aria-hidden />
|
return <div className={className} style={{ height: bannerPx ?? 0 }} aria-hidden />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import type { MapVoteState } from '../../../types/mapvote'
|
|||||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||||
import { Tabs } from './Tabs'
|
import { Tabs } from './Tabs'
|
||||||
import Chart from './Chart'
|
import Chart from './Chart'
|
||||||
|
import { MAPVOTE_REFRESH } from '@/lib/sseEvents'
|
||||||
|
|
||||||
/* =================== Utilities & constants =================== */
|
/* =================== Utilities & constants =================== */
|
||||||
|
|
||||||
@ -189,32 +190,6 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
|
|
||||||
useEffect(() => { load() }, [load])
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!lastEvent) return
|
|
||||||
|
|
||||||
const unwrap = (e: any) => e?.payload?.payload ?? e?.payload ?? e
|
|
||||||
const evt = unwrap(lastEvent)
|
|
||||||
const type = lastEvent.type ?? evt?.type
|
|
||||||
|
|
||||||
if (evt?.matchId !== match.id) return
|
|
||||||
|
|
||||||
if (type === 'map-vote-updated') {
|
|
||||||
const { opensAt, leadMinutes } = evt ?? {}
|
|
||||||
if (opensAt) {
|
|
||||||
const ts = new Date(opensAt).getTime()
|
|
||||||
if (Number.isFinite(ts)) setOpensAtOverrideTs(ts)
|
|
||||||
setState(prev => (prev ? { ...prev, opensAt } : prev))
|
|
||||||
} else if (Number.isFinite(leadMinutes) && matchBaseTs != null) {
|
|
||||||
const ts = matchBaseTs - Number(leadMinutes) * 60_000
|
|
||||||
setOpensAtOverrideTs(ts)
|
|
||||||
setState(prev => (prev ? { ...prev, opensAt: new Date(ts).toISOString() } : prev))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAPVOTE_REFRESH = new Set(['map-vote-updated', 'map-vote-reset', 'map-vote-admin-edit'])
|
|
||||||
if (MAPVOTE_REFRESH.has(type)) load()
|
|
||||||
}, [lastEvent, match.id, load, matchBaseTs])
|
|
||||||
|
|
||||||
/* -------- Admin-Edit Mirror -------- */
|
/* -------- Admin-Edit Mirror -------- */
|
||||||
const adminEditingBy = state?.adminEdit?.by ?? null
|
const adminEditingBy = state?.adminEdit?.by ?? null
|
||||||
const adminEditingEnabled = !!state?.adminEdit?.enabled
|
const adminEditingEnabled = !!state?.adminEdit?.enabled
|
||||||
@ -227,8 +202,10 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
const me = session?.user
|
const me = session?.user
|
||||||
const isAdmin = !!me?.isAdmin
|
const isAdmin = !!me?.isAdmin
|
||||||
const mySteamId = me?.steamId
|
const mySteamId = me?.steamId
|
||||||
const isLeaderA = !!mySteamId && match.teamA?.leader?.steamId === mySteamId
|
const leaderAId = state?.teams?.teamA?.leader?.steamId ?? match.teamA?.leader?.steamId ?? null
|
||||||
const isLeaderB = !!mySteamId && match.teamB?.leader?.steamId === mySteamId
|
const leaderBId = state?.teams?.teamB?.leader?.steamId ?? match.teamB?.leader?.steamId ?? null
|
||||||
|
const isLeaderA = !!mySteamId && leaderAId === mySteamId
|
||||||
|
const isLeaderB = !!mySteamId && leaderBId === mySteamId
|
||||||
|
|
||||||
const isFrozenByAdmin = adminEditingEnabled && adminEditingBy !== mySteamId
|
const isFrozenByAdmin = adminEditingEnabled && adminEditingBy !== mySteamId
|
||||||
|
|
||||||
@ -457,6 +434,62 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
[playersB]
|
[playersB]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ⬇️ Ergänzen: Hilfsmenge aller Spieler-IDs dieses Matches
|
||||||
|
const teamSteamIds = useMemo(
|
||||||
|
() => new Set([
|
||||||
|
...playersA.map(p => p.user.steamId),
|
||||||
|
...playersB.map(p => p.user.steamId),
|
||||||
|
]),
|
||||||
|
[playersA, playersB]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Optimistisches Update: Leader im lokalen state tauschen
|
||||||
|
const applyLeaderChange = useCallback(
|
||||||
|
(teamIdFromEvt: string | null, newLeaderSteamId: string | null) => {
|
||||||
|
if (!state || !newLeaderSteamId) return;
|
||||||
|
|
||||||
|
// herausfinden, welches Team gemeint ist (per teamId oder über Players)
|
||||||
|
const teamKey: 'teamA' | 'teamB' | null =
|
||||||
|
teamIdFromEvt
|
||||||
|
? (state?.teams?.teamA?.id === teamIdFromEvt
|
||||||
|
? 'teamA'
|
||||||
|
: state?.teams?.teamB?.id === teamIdFromEvt
|
||||||
|
? 'teamB'
|
||||||
|
: null)
|
||||||
|
: (playersA.some(p => p.user.steamId === newLeaderSteamId)
|
||||||
|
? 'teamA'
|
||||||
|
: playersB.some(p => p.user.steamId === newLeaderSteamId)
|
||||||
|
? 'teamB'
|
||||||
|
: null);
|
||||||
|
|
||||||
|
if (!teamKey) return;
|
||||||
|
|
||||||
|
setState(prev => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const oldId = prev.teams?.[teamKey]?.leader?.steamId;
|
||||||
|
if (oldId === newLeaderSteamId) return prev; // nichts zu tun
|
||||||
|
|
||||||
|
const poolPlayers = prev.teams?.[teamKey]?.players ?? [];
|
||||||
|
const found = poolPlayers.find(p => p.steamId === newLeaderSteamId);
|
||||||
|
const leader = found
|
||||||
|
? { steamId: found.steamId, name: found.name, avatar: found.avatar }
|
||||||
|
: { steamId: newLeaderSteamId };
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
teams: {
|
||||||
|
...prev.teams,
|
||||||
|
[teamKey]: {
|
||||||
|
...prev.teams?.[teamKey],
|
||||||
|
leader,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as MapVoteState;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[state, playersA, playersB, setState]
|
||||||
|
);
|
||||||
|
|
||||||
const amInTeamA = !!mySteamId && playersA.some(p => p.user?.steamId === mySteamId)
|
const amInTeamA = !!mySteamId && playersA.some(p => p.user?.steamId === mySteamId)
|
||||||
const amInTeamB = !!mySteamId && playersB.some(p => p.user?.steamId === mySteamId)
|
const amInTeamB = !!mySteamId && playersB.some(p => p.user?.steamId === mySteamId)
|
||||||
const myTeamId = amInTeamA ? match.teamA?.id : amInTeamB ? match.teamB?.id : null
|
const myTeamId = amInTeamA ? match.teamA?.id : amInTeamB ? match.teamB?.id : null
|
||||||
@ -601,6 +634,96 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
[mapsKey] // reicht, solange keys unverändert
|
[mapsKey] // reicht, solange keys unverändert
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!lastEvent) return
|
||||||
|
|
||||||
|
const unwrap = (e: any) => e?.payload?.payload ?? e?.payload ?? e
|
||||||
|
const evt = unwrap(lastEvent)
|
||||||
|
const type = lastEvent.type ?? evt?.type
|
||||||
|
|
||||||
|
const evtMatchId: string | null = evt?.matchId ?? null
|
||||||
|
const evtTeamId : string | null = evt?.teamId ?? null
|
||||||
|
const actionType: string | null = evt?.actionType ?? null
|
||||||
|
const actionData: string | null = evt?.actionData ?? null // z.B. newLeaderSteamId
|
||||||
|
|
||||||
|
// 1) Relevanz wie bisher
|
||||||
|
const isForThisMatchByMatchId =
|
||||||
|
!!evtMatchId && evtMatchId === match.id
|
||||||
|
|
||||||
|
const isForThisMatchByTeamId =
|
||||||
|
!!evtTeamId && (evtTeamId === match.teamA?.id || evtTeamId === match.teamB?.id)
|
||||||
|
|
||||||
|
// 2) Notifications abdecken (changed + self)
|
||||||
|
const isLeaderChangeNotification =
|
||||||
|
type === 'notification' && (actionType === 'team-leader-changed' || actionType === 'team-leader-self')
|
||||||
|
|
||||||
|
// 3) Gehört der neue Leader zu unseren Teams?
|
||||||
|
const byNewLeaderId =
|
||||||
|
isLeaderChangeNotification && !!actionData && teamSteamIds.has(actionData)
|
||||||
|
|
||||||
|
// 4) Relevanz
|
||||||
|
const isRelevant = isForThisMatchByMatchId || isForThisMatchByTeamId || byNewLeaderId
|
||||||
|
|
||||||
|
// 5) Offensichtliche Leader-Änderungen -> hart refreshen, auch ohne Relevanzbeweis
|
||||||
|
const forceRefresh =
|
||||||
|
type === 'team-leader-changed' ||
|
||||||
|
type === 'team-updated' ||
|
||||||
|
isLeaderChangeNotification
|
||||||
|
|
||||||
|
if (!isRelevant && !forceRefresh) return
|
||||||
|
|
||||||
|
// map-vote-updated: opensAt-Override wie gehabt ...
|
||||||
|
if (type === 'map-vote-updated') {
|
||||||
|
const { opensAt, leadMinutes } = evt ?? {}
|
||||||
|
if (opensAt) {
|
||||||
|
const ts = new Date(opensAt).getTime()
|
||||||
|
if (Number.isFinite(ts)) setOpensAtOverrideTs(ts)
|
||||||
|
setState(prev => (prev ? { ...prev, opensAt } : prev))
|
||||||
|
} else if (Number.isFinite(leadMinutes) && matchBaseTs != null) {
|
||||||
|
const ts = matchBaseTs - Number(leadMinutes) * 60_000
|
||||||
|
setOpensAtOverrideTs(ts)
|
||||||
|
setState(prev => (prev ? { ...prev, opensAt: new Date(ts).toISOString() } : prev))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Leader-Events zuerst lokal anwenden (ohne Reload) ----------------------
|
||||||
|
const isLeaderChangeEvent =
|
||||||
|
type === 'team-leader-changed' ||
|
||||||
|
(type === 'notification' &&
|
||||||
|
(actionType === 'team-leader-changed' || actionType === 'team-leader-self'));
|
||||||
|
|
||||||
|
if (isLeaderChangeEvent) {
|
||||||
|
applyLeaderChange(evtTeamId ?? null, actionData ?? null);
|
||||||
|
return; // kein load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- map-vote-/sonstige Events wie gehabt -----------------------------------
|
||||||
|
let shouldRefresh = MAPVOTE_REFRESH.has(type);
|
||||||
|
|
||||||
|
// Optional: harte Reloads weiter einschränken, falls gewünscht
|
||||||
|
// (z.B. kein Reload für "team-updated", wenn dich nur Leader interessiert)
|
||||||
|
// if (type === 'team-updated') shouldRefresh = false;
|
||||||
|
|
||||||
|
if (type === 'map-vote-updated') {
|
||||||
|
const { opensAt, leadMinutes } = evt ?? {};
|
||||||
|
if (opensAt) {
|
||||||
|
const ts = new Date(opensAt).getTime();
|
||||||
|
if (Number.isFinite(ts)) setOpensAtOverrideTs(ts);
|
||||||
|
setState(prev => (prev ? { ...prev, opensAt } : prev));
|
||||||
|
} else if (Number.isFinite(leadMinutes) && matchBaseTs != null) {
|
||||||
|
const ts = matchBaseTs - Number(leadMinutes) * 60_000;
|
||||||
|
setOpensAtOverrideTs(ts);
|
||||||
|
setState(prev => (prev ? { ...prev, opensAt: new Date(ts).toISOString() } : prev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRefresh) {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}, [lastEvent, match.id, match.teamA?.id, match.teamB?.id, load, matchBaseTs, teamSteamIds])
|
||||||
|
|
||||||
// Effect NUR an stabile Keys + Tab hängen
|
// Effect NUR an stabile Keys + Tab hängen
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tab !== 'winrate') return;
|
if (tab !== 'winrate') return;
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import Link from 'next/link'
|
|||||||
import Card from './Card'
|
import Card from './Card'
|
||||||
import MiniPlayerCard from './MiniPlayerCard'
|
import MiniPlayerCard from './MiniPlayerCard'
|
||||||
import type { PlayerSummary } from './MiniPlayerCard'
|
import type { PlayerSummary } from './MiniPlayerCard'
|
||||||
|
import UserAvatarWithStatus from './UserAvatarWithStatus'
|
||||||
|
|
||||||
// Für den Prefetch greifen wir dieselbe Performance-Logik auf:
|
// Für den Prefetch greifen wir dieselbe Performance-Logik auf:
|
||||||
const KD_CAP = 2.0
|
const KD_CAP = 2.0
|
||||||
@ -623,7 +624,8 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
|||||||
players: MatchPlayer[],
|
players: MatchPlayer[],
|
||||||
teamTitle: string,
|
teamTitle: string,
|
||||||
showEdit: boolean,
|
showEdit: boolean,
|
||||||
onEditClick?: () => void
|
onEditClick?: () => void,
|
||||||
|
leaderSteamId?: string | null,
|
||||||
) => {
|
) => {
|
||||||
const sorted = [...players].sort(
|
const sorted = [...players].sort(
|
||||||
(a, b) => (b.stats?.totalDamage ?? 0) - (a.stats?.totalDamage ?? 0),
|
(a, b) => (b.stats?.totalDamage ?? 0) - (a.stats?.totalDamage ?? 0),
|
||||||
@ -672,6 +674,7 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
|||||||
{sorted.map((p) => {
|
{sorted.map((p) => {
|
||||||
const banned = isPlayerBanned(p);
|
const banned = isPlayerBanned(p);
|
||||||
const title = banned ? banTooltip(p) : undefined;
|
const title = banned ? banTooltip(p) : undefined;
|
||||||
|
const isLeader = p.user.steamId === leaderSteamId // ⬅️ hier prüfen
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Row key={p.user.steamId} title={title} className={`${banned ? 'bg-red-900/20' : ''}`}>
|
<Table.Row key={p.user.steamId} title={title} className={`${banned ? 'bg-red-900/20' : ''}`}>
|
||||||
@ -692,10 +695,15 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
|||||||
}}
|
}}
|
||||||
onClick={() => router.push(`/profile/${p.user.steamId}`)}
|
onClick={() => router.push(`/profile/${p.user.steamId}`)}
|
||||||
>
|
>
|
||||||
<img
|
<UserAvatarWithStatus
|
||||||
|
steamId={p.user.steamId}
|
||||||
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
||||||
alt={p.user.name}
|
alt={p.user.name}
|
||||||
|
size={36}
|
||||||
|
isLeader={isLeader}
|
||||||
|
showStatus={false}
|
||||||
className="mr-3 h-8 w-8 rounded-full"
|
className="mr-3 h-8 w-8 rounded-full"
|
||||||
|
alignRight={false}
|
||||||
/>
|
/>
|
||||||
<div className="text-base font-semibold flex items-center gap-2">
|
<div className="text-base font-semibold flex items-center gap-2">
|
||||||
<span>{p.user.name ?? 'Unbekannt'}</span>
|
<span>{p.user.name ?? 'Unbekannt'}</span>
|
||||||
@ -963,12 +971,12 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
|||||||
{/* Teams / Tabellen */}
|
{/* Teams / Tabellen */}
|
||||||
{/* Team A */}
|
{/* Team A */}
|
||||||
<div>
|
<div>
|
||||||
{renderTable(teamAPlayers, teamATitle, showEditA, () => setEditSide('A'))}
|
{renderTable(teamAPlayers, teamATitle, showEditA, () => setEditSide('A'), match.teamA?.leader?.steamId)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Team B */}
|
{/* Team B */}
|
||||||
<div>
|
<div>
|
||||||
{renderTable(teamBPlayers, teamBTitle, showEditB, () => setEditSide('B'))}
|
{renderTable(teamBPlayers, teamBTitle, showEditB, () => setEditSide('B'), match.teamB?.leader?.steamId)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Echte Modals (außerhalb IIFE, State oben): */}
|
{/* Echte Modals (außerhalb IIFE, State oben): */}
|
||||||
|
|||||||
@ -79,6 +79,7 @@ export default function MiniCard({
|
|||||||
const cardClasses = `
|
const cardClasses = `
|
||||||
relative flex flex-col items-center p-4 border rounded-lg transition
|
relative flex flex-col items-center p-4 border rounded-lg transition
|
||||||
max-h-[200px] max-w-[160px] overflow-hidden shadow-2xs rounded-xl
|
max-h-[200px] max-w-[160px] overflow-hidden shadow-2xs rounded-xl
|
||||||
|
hover:bg-neutral-400 hover:dark:bg-neutral-700 hover:shadow-md
|
||||||
${statusBg} ${baseBorder}
|
${statusBg} ${baseBorder}
|
||||||
${hoverEffect ? 'hover:cursor-grab hover:scale-105' : ''}
|
${hoverEffect ? 'hover:cursor-grab hover:scale-105' : ''}
|
||||||
${isSelectable ? 'cursor-pointer' : ''}
|
${isSelectable ? 'cursor-pointer' : ''}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { useSession } from 'next-auth/react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useSSEStore } from '@/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import { NOTIFICATION_EVENTS, isSseEventType } from '@/lib/sseEvents'
|
import { NOTIFICATION_EVENTS, isSseEventType } from '@/lib/sseEvents'
|
||||||
import { useUiChromeStore } from '@/lib/useUiChromeStore'
|
import { useGameBannerStore } from '@/lib/useGameBannerStore'
|
||||||
|
|
||||||
type Notification = {
|
type Notification = {
|
||||||
id: string
|
id: string
|
||||||
@ -39,7 +39,7 @@ export default function NotificationBell() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { lastEvent } = useSSEStore() // nur konsumieren, nicht connecten
|
const { lastEvent } = useSSEStore() // nur konsumieren, nicht connecten
|
||||||
const bellRef = useRef<HTMLButtonElement | null>(null);
|
const bellRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const telemetryBannerPx = useUiChromeStore(s => s.gameBannerPx)
|
const telemetryBannerPx = useGameBannerStore(s => s.gameBannerPx)
|
||||||
|
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([])
|
const [notifications, setNotifications] = useState<Notification[]>([])
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|||||||
@ -156,20 +156,42 @@ export default function TelemetrySocket() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
aliveRef.current = true
|
aliveRef.current = true
|
||||||
|
|
||||||
const connect = () => {
|
let wsLocal: WebSocket | null = null // <- dieses WS schließen wir im Cleanup
|
||||||
|
const connectOnce = () => {
|
||||||
if (!aliveRef.current || !url) return
|
if (!aliveRef.current || !url) return
|
||||||
|
|
||||||
|
// ✅ Guard: nicht verbinden, wenn schon OPEN oder CONNECTING
|
||||||
|
if (wsRef.current && (
|
||||||
|
wsRef.current.readyState === WebSocket.OPEN ||
|
||||||
|
wsRef.current.readyState === WebSocket.CONNECTING
|
||||||
|
)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const ws = new WebSocket(url)
|
const ws = new WebSocket(url)
|
||||||
|
wsLocal = ws
|
||||||
wsRef.current = ws
|
wsRef.current = ws
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] open')
|
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] open')
|
||||||
|
// ✅ evtl. bestehenden Reconnect-Timer löschen
|
||||||
|
if (retryRef.current) { window.clearTimeout(retryRef.current); retryRef.current = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] error')
|
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] error')
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] closed')
|
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] closed')
|
||||||
if (aliveRef.current) retryRef.current = window.setTimeout(connect, 2000)
|
wsRef.current = null
|
||||||
|
// ✅ nur EINEN Reconnect-Timer setzen
|
||||||
|
if (aliveRef.current && !retryRef.current) {
|
||||||
|
retryRef.current = window.setTimeout(() => {
|
||||||
|
retryRef.current = null
|
||||||
|
connectOnce()
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onmessage = (ev) => {
|
ws.onmessage = (ev) => {
|
||||||
@ -241,13 +263,13 @@ export default function TelemetrySocket() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connect()
|
connectOnce()
|
||||||
return () => {
|
return () => {
|
||||||
aliveRef.current = false
|
aliveRef.current = false
|
||||||
if (retryRef.current) window.clearTimeout(retryRef.current)
|
if (retryRef.current) { window.clearTimeout(retryRef.current); retryRef.current = null }
|
||||||
try { wsRef.current?.close(1000, 'telemetry socket unmounted') } catch {}
|
try { wsRef.current?.close(1000, 'telemetry socket unmounted') } catch {}
|
||||||
}
|
}
|
||||||
}, [url, setSnapshot, setJoin, setLeave, setMapKey, setPhase, hideOverlay, mySteamId])
|
}, [url])
|
||||||
|
|
||||||
|
|
||||||
// Wenn die API { matchId: null } liefert → KEIN Banner
|
// Wenn die API { matchId: null } liefert → KEIN Banner
|
||||||
|
|||||||
@ -5,10 +5,11 @@ import { useEffect, useState, useMemo } from 'react'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useSSEStore } from '@/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
|
import type React from 'react'
|
||||||
|
|
||||||
type Presence = 'online' | 'away' | 'offline'
|
type Presence = 'online' | 'away' | 'offline'
|
||||||
|
|
||||||
type Props = {
|
type Props = React.HTMLAttributes<HTMLDivElement> & {
|
||||||
steamId?: string | null
|
steamId?: string | null
|
||||||
src: string
|
src: string
|
||||||
alt?: string
|
alt?: string
|
||||||
@ -17,6 +18,8 @@ type Props = {
|
|||||||
isLeader?: boolean
|
isLeader?: boolean
|
||||||
alignRight?: boolean
|
alignRight?: boolean
|
||||||
showStatus?: boolean
|
showStatus?: boolean
|
||||||
|
/** Zusätzliche Klassen für den runden Avatar-/Ring-Container */
|
||||||
|
avatarClassName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserAvatarWithStatus({
|
export default function UserAvatarWithStatus({
|
||||||
@ -28,21 +31,20 @@ export default function UserAvatarWithStatus({
|
|||||||
isLeader = false,
|
isLeader = false,
|
||||||
alignRight = false,
|
alignRight = false,
|
||||||
showStatus = true,
|
showStatus = true,
|
||||||
|
className, // <— NEU: Klassen für den äußeren Wrapper
|
||||||
|
avatarClassName, // <— NEU: Klassen für den inneren Avatar-Container
|
||||||
|
...rest // <— NEU: alle weiteren div-Props (onClick, title, …)
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { lastEvent } = useSSEStore()
|
const { lastEvent } = useSSEStore()
|
||||||
const [status, setStatus] = useState<Presence>(initialStatus)
|
const [status, setStatus] = useState<Presence>(initialStatus)
|
||||||
|
|
||||||
// Dynamik für Krone (Skalierung & Offset relativ zu size)
|
|
||||||
const { crownSize, crownOffset, crownIconSize } = useMemo(() => {
|
const { crownSize, crownOffset, crownIconSize } = useMemo(() => {
|
||||||
// Button-Durchmesser max. 20px
|
|
||||||
const cs = Math.min(20, Math.round(Math.max(size * 0.5, 14)))
|
const cs = Math.min(20, Math.round(Math.max(size * 0.5, 14)))
|
||||||
const off = Math.round(size * 0.10) // Offset bleibt dynamisch
|
const off = Math.round(size * 0.10)
|
||||||
// Icon etwas kleiner (z.B. 70% vom Button)
|
|
||||||
const icon = Math.round(cs * 0.70)
|
const icon = Math.round(cs * 0.70)
|
||||||
return { crownSize: cs, crownOffset: off, crownIconSize: icon }
|
return { crownSize: cs, crownOffset: off, crownIconSize: icon }
|
||||||
}, [size])
|
}, [size])
|
||||||
|
|
||||||
// initialer Status
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showStatus || !steamId) return
|
if (!showStatus || !steamId) return
|
||||||
let alive = true
|
let alive = true
|
||||||
@ -57,15 +59,12 @@ export default function UserAvatarWithStatus({
|
|||||||
return () => { alive = false }
|
return () => { alive = false }
|
||||||
}, [steamId, showStatus])
|
}, [steamId, showStatus])
|
||||||
|
|
||||||
// SSE-Updates
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showStatus || !steamId) return
|
if (!showStatus || !steamId) return
|
||||||
if (!lastEvent || lastEvent.type !== 'user-status-updated') return
|
if (!lastEvent || lastEvent.type !== 'user-status-updated') return
|
||||||
|
|
||||||
const raw = lastEvent.payload as any
|
const raw = lastEvent.payload as any
|
||||||
const sid = raw?.steamId ?? raw?.payload?.steamId
|
const sid = raw?.steamId ?? raw?.payload?.steamId
|
||||||
const st = (raw?.status ?? raw?.payload?.status) as Presence | undefined
|
const st = (raw?.status ?? raw?.payload?.status) as Presence | undefined
|
||||||
|
|
||||||
if (sid === steamId && st) setStatus(st)
|
if (sid === steamId && st) setStatus(st)
|
||||||
}, [lastEvent, steamId, showStatus])
|
}, [lastEvent, steamId, showStatus])
|
||||||
|
|
||||||
@ -78,15 +77,17 @@ export default function UserAvatarWithStatus({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative items-center justify-center"
|
className={clsx('relative inline-flex items-center justify-center', className)}
|
||||||
style={{ width: size, height: size }}
|
style={{ width: size, height: size }}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex items-center justify-center rounded-full transition',
|
'flex items-center justify-center rounded-full transition',
|
||||||
showStatus
|
showStatus
|
||||||
? ['ring-1 ring-offset-1 ring-offset-white dark:ring-offset-neutral-900', statusRing]
|
? ['ring-1 ring-offset-1 ring-offset-white dark:ring-offset-neutral-900', statusRing]
|
||||||
: null
|
: null,
|
||||||
|
avatarClassName
|
||||||
)}
|
)}
|
||||||
style={{ width: size, height: size }}
|
style={{ width: size, height: size }}
|
||||||
>
|
>
|
||||||
@ -99,7 +100,6 @@ export default function UserAvatarWithStatus({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Team-Leader-Krone – skaliert & positioniert relativ zur Avatargröße */}
|
|
||||||
{isLeader && (
|
{isLeader && (
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -116,7 +116,6 @@ export default function UserAvatarWithStatus({
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height={crownIconSize} width={crownIconSize} fill="currentColor" viewBox="0 0 640 640">
|
<svg xmlns="http://www.w3.org/2000/svg" height={crownIconSize} width={crownIconSize} fill="currentColor" viewBox="0 0 640 640">
|
||||||
<path d="M345 151.2C354.2 143.9 360 132.6 360 120C360 97.9 342.1 80 320 80C297.9 80 280 97.9 280 120C280 132.6 285.9 143.9 295 151.2L226.6 258.8C216.6 274.5 195.3 278.4 180.4 267.2L120.9 222.7C125.4 216.3 128 208.4 128 200C128 177.9 110.1 160 88 160C65.9 160 48 177.9 48 200C48 221.8 65.5 239.6 87.2 240L119.8 457.5C124.5 488.8 151.4 512 183.1 512L456.9 512C488.6 512 515.5 488.8 520.2 457.5L552.8 240C574.5 239.6 592 221.8 592 200C592 177.9 574.1 160 552 160C529.9 160 512 177.9 512 200C512 208.4 514.6 216.3 519.1 222.7L459.7 267.3C444.8 278.5 423.5 274.6 413.5 258.9L345 151.2z"/>
|
<path d="M345 151.2C354.2 143.9 360 132.6 360 120C360 97.9 342.1 80 320 80C297.9 80 280 97.9 280 120C280 132.6 285.9 143.9 295 151.2L226.6 258.8C216.6 274.5 195.3 278.4 180.4 267.2L120.9 222.7C125.4 216.3 128 208.4 128 200C128 177.9 110.1 160 88 160C65.9 160 48 177.9 48 200C48 221.8 65.5 239.6 87.2 240L119.8 457.5C124.5 488.8 151.4 512 183.1 512L456.9 512C488.6 512 515.5 488.8 520.2 457.5L552.8 240C574.5 239.6 592 221.8 592 200C592 177.9 574.1 160 552 160C529.9 160 512 177.9 512 200C512 208.4 514.6 216.3 519.1 222.7L459.7 267.3C444.8 278.5 423.5 274.6 413.5 258.9L345 151.2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import AudioPrimer from './components/AudioPrimer';
|
|||||||
import ReadyOverlayHost from './components/ReadyOverlayHost';
|
import ReadyOverlayHost from './components/ReadyOverlayHost';
|
||||||
import TelemetrySocket from './components/TelemetrySocket';
|
import TelemetrySocket from './components/TelemetrySocket';
|
||||||
import GameBannerSpacer from './components/GameBannerSpacer';
|
import GameBannerSpacer from './components/GameBannerSpacer';
|
||||||
|
import GameBannerHost from './components/GameBannerHost';
|
||||||
|
|
||||||
const geistSans = Geist({variable: '--font-geist-sans', subsets: ['latin']});
|
const geistSans = Geist({variable: '--font-geist-sans', subsets: ['latin']});
|
||||||
const geistMono = Geist_Mono({variable: '--font-geist-mono', subsets: ['latin']});
|
const geistMono = Geist_Mono({variable: '--font-geist-mono', subsets: ['latin']});
|
||||||
@ -65,6 +66,7 @@ export default async function RootLayout({children, params}: Props) {
|
|||||||
<main className="flex-1 min-w-0 min-h-0 overflow-auto">
|
<main className="flex-1 min-w-0 min-h-0 overflow-auto">
|
||||||
<div className="h-full min-h-0 box-border p-4 sm:p-6">{children}</div>
|
<div className="h-full min-h-0 box-border p-4 sm:p-6">{children}</div>
|
||||||
</main>
|
</main>
|
||||||
|
<GameBannerHost />
|
||||||
<GameBannerSpacer className="hidden sm:block" />
|
<GameBannerSpacer className="hidden sm:block" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { authOptions } from '@/lib/auth'
|
|||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { MapVoteAction } from '@/generated/prisma'
|
import { MapVoteAction } from '@/generated/prisma'
|
||||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||||
import { randomInt, createHash, randomBytes } from 'crypto'
|
import { randomInt, createHash } from 'crypto'
|
||||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||||
|
|
||||||
export const runtime = 'nodejs'
|
export const runtime = 'nodejs'
|
||||||
@ -469,7 +469,7 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
|||||||
if (!sLike.locked) return
|
if (!sLike.locked) return
|
||||||
const bestOf = (mLike.bestOf ?? sLike.bestOf ?? (match.matchType === 'community' ? 3 : 1))
|
const bestOf = (mLike.bestOf ?? sLike.bestOf ?? (match.matchType === 'community' ? 3 : 1))
|
||||||
const chosen = (sLike.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
const chosen = (sLike.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
||||||
if (chosen.length < bestOf) return
|
if (chosen.length < bestOf) return false
|
||||||
|
|
||||||
// ⬇️ JSON bauen (enthält cs2MatchId/rndId)
|
// ⬇️ JSON bauen (enthält cs2MatchId/rndId)
|
||||||
const json = buildMatchJson(mLike, sLike)
|
const json = buildMatchJson(mLike, sLike)
|
||||||
@ -511,8 +511,6 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
|||||||
// … dann das neue laden
|
// … dann das neue laden
|
||||||
await sendServerCommand(`matchzy_loadmatch ${filename}`)
|
await sendServerCommand(`matchzy_loadmatch ${filename}`)
|
||||||
|
|
||||||
// Spieler persistieren + cs2MatchId speichern wie gehabt
|
|
||||||
await persistMatchPlayers(match)
|
|
||||||
if (typeof json.matchid === 'number') {
|
if (typeof json.matchid === 'number') {
|
||||||
await prisma.match.update({
|
await prisma.match.update({
|
||||||
where: { id: match.id },
|
where: { id: match.id },
|
||||||
@ -524,8 +522,11 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
|||||||
data: { exportedAt: new Date() },
|
data: { exportedAt: new Date() },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[mapvote] Export fehlgeschlagen:', err)
|
console.error('[mapvote] Export fehlgeschlagen:', err)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,6 +555,17 @@ async function writeLiveStateToServerConfig(match: any, vote: any, mapVisuals: R
|
|||||||
bannerExpiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000),
|
bannerExpiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await sendServerSSEMessage({
|
||||||
|
type: 'server-config-updated',
|
||||||
|
payload: {
|
||||||
|
activeMatchId: match.id,
|
||||||
|
activeMapKey: key,
|
||||||
|
activeMapLabel: label,
|
||||||
|
activeMapBg: bg,
|
||||||
|
activeParticipants: participants,
|
||||||
|
},
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[mapvote] writeLiveStateToServerConfig failed:', e)
|
console.warn('[mapvote] writeLiveStateToServerConfig failed:', e)
|
||||||
}
|
}
|
||||||
@ -561,15 +573,20 @@ async function writeLiveStateToServerConfig(match: any, vote: any, mapVisuals: R
|
|||||||
|
|
||||||
/** DRY: Wird in jedem "locked"-Pfad aufgerufen */
|
/** DRY: Wird in jedem "locked"-Pfad aufgerufen */
|
||||||
async function afterVoteLocked(match: any, vote: any, mapVisuals: Record<string, any>) {
|
async function afterVoteLocked(match: any, vote: any, mapVisuals: Record<string, any>) {
|
||||||
// 1) Spieler für dieses Match festschreiben
|
// 1) Spieler festschreiben
|
||||||
await persistMatchPlayers(match)
|
await persistMatchPlayers(match)
|
||||||
// 2) Live-State für Banner speichern
|
|
||||||
await writeLiveStateToServerConfig(match, vote, mapVisuals)
|
// 2) Serverexport + Matchzy-Load
|
||||||
// 3) Serverexport + Matchzy-Load
|
const ok = await exportMatchToSftpDirect(match, vote) // ← wartet bis Load versucht wurde
|
||||||
await exportMatchToSftpDirect(match, vote)
|
|
||||||
|
// 3) Nur wenn Export/Load ok war: Live-State + SSE "server-config-updated"
|
||||||
|
if (ok) {
|
||||||
|
await writeLiveStateToServerConfig(match, vote, mapVisuals)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ---------- kleine Helfer für match-ready Payload ---------- */
|
/* ---------- kleine Helfer für match-ready Payload ---------- */
|
||||||
|
|
||||||
function deriveChosenSteps(vote: any) {
|
function deriveChosenSteps(vote: any) {
|
||||||
@ -697,9 +714,6 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
});
|
});
|
||||||
|
|
||||||
await afterVoteLocked(match, updated, mapVisuals)
|
await afterVoteLocked(match, updated, mapVisuals)
|
||||||
|
|
||||||
// Export serverseitig
|
|
||||||
await exportMatchToSftpDirect(match, updated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
||||||
@ -762,9 +776,6 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
});
|
});
|
||||||
|
|
||||||
await afterVoteLocked(match, updated, mapVisuals)
|
await afterVoteLocked(match, updated, mapVisuals)
|
||||||
|
|
||||||
// Export serverseitig
|
|
||||||
await exportMatchToSftpDirect(match, updated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
||||||
@ -865,8 +876,6 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
});
|
});
|
||||||
|
|
||||||
await afterVoteLocked(match, updated, mapVisuals)
|
await afterVoteLocked(match, updated, mapVisuals)
|
||||||
|
|
||||||
await exportMatchToSftpDirect(match, updated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
||||||
|
|||||||
@ -97,7 +97,6 @@ export async function POST(req: NextRequest) {
|
|||||||
})))
|
})))
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({
|
||||||
type: 'team-leader-changed',
|
type: 'team-leader-changed',
|
||||||
targetUserIds: others,
|
|
||||||
teamId,
|
teamId,
|
||||||
message: textForOthers,
|
message: textForOthers,
|
||||||
actionData: newLeaderSteamId,
|
actionData: newLeaderSteamId,
|
||||||
|
|||||||
@ -35,12 +35,12 @@ exports.Prisma = Prisma
|
|||||||
exports.$Enums = {}
|
exports.$Enums = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prisma Client JS version: 6.16.3
|
* Prisma Client JS version: 6.17.0
|
||||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||||
*/
|
*/
|
||||||
Prisma.prismaVersion = {
|
Prisma.prismaVersion = {
|
||||||
client: "6.16.3",
|
client: "6.17.0",
|
||||||
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
|
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
|
||||||
}
|
}
|
||||||
|
|
||||||
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
||||||
@ -412,7 +412,7 @@ const config = {
|
|||||||
"value": "prisma-client-js"
|
"value": "prisma-client-js"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||||
"fromEnvVar": null
|
"fromEnvVar": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@ -426,7 +426,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"previewFeatures": [],
|
"previewFeatures": [],
|
||||||
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
@ -434,8 +434,8 @@ const config = {
|
|||||||
"schemaEnvPath": "../../../.env"
|
"schemaEnvPath": "../../../.env"
|
||||||
},
|
},
|
||||||
"relativePath": "../../../prisma",
|
"relativePath": "../../../prisma",
|
||||||
"clientVersion": "6.16.3",
|
"clientVersion": "6.17.0",
|
||||||
"engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||||
"datasourceNames": [
|
"datasourceNames": [
|
||||||
"db"
|
"db"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -20,12 +20,12 @@ exports.Prisma = Prisma
|
|||||||
exports.$Enums = {}
|
exports.$Enums = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prisma Client JS version: 6.16.3
|
* Prisma Client JS version: 6.17.0
|
||||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||||
*/
|
*/
|
||||||
Prisma.prismaVersion = {
|
Prisma.prismaVersion = {
|
||||||
client: "6.16.3",
|
client: "6.17.0",
|
||||||
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
|
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
|
||||||
}
|
}
|
||||||
|
|
||||||
Prisma.PrismaClientKnownRequestError = () => {
|
Prisma.PrismaClientKnownRequestError = () => {
|
||||||
|
|||||||
4
src/generated/prisma/index.d.ts
vendored
4
src/generated/prisma/index.d.ts
vendored
@ -487,8 +487,8 @@ export namespace Prisma {
|
|||||||
export import Exact = $Public.Exact
|
export import Exact = $Public.Exact
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prisma Client JS version: 6.16.3
|
* Prisma Client JS version: 6.17.0
|
||||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||||
*/
|
*/
|
||||||
export type PrismaVersion = {
|
export type PrismaVersion = {
|
||||||
client: string
|
client: string
|
||||||
|
|||||||
@ -35,12 +35,12 @@ exports.Prisma = Prisma
|
|||||||
exports.$Enums = {}
|
exports.$Enums = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prisma Client JS version: 6.16.3
|
* Prisma Client JS version: 6.17.0
|
||||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||||
*/
|
*/
|
||||||
Prisma.prismaVersion = {
|
Prisma.prismaVersion = {
|
||||||
client: "6.16.3",
|
client: "6.17.0",
|
||||||
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
|
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
|
||||||
}
|
}
|
||||||
|
|
||||||
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
||||||
@ -413,7 +413,7 @@ const config = {
|
|||||||
"value": "prisma-client-js"
|
"value": "prisma-client-js"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||||
"fromEnvVar": null
|
"fromEnvVar": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@ -427,7 +427,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"previewFeatures": [],
|
"previewFeatures": [],
|
||||||
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
@ -435,8 +435,8 @@ const config = {
|
|||||||
"schemaEnvPath": "../../../.env"
|
"schemaEnvPath": "../../../.env"
|
||||||
},
|
},
|
||||||
"relativePath": "../../../prisma",
|
"relativePath": "../../../prisma",
|
||||||
"clientVersion": "6.16.3",
|
"clientVersion": "6.17.0",
|
||||||
"engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||||
"datasourceNames": [
|
"datasourceNames": [
|
||||||
"db"
|
"db"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -151,7 +151,7 @@
|
|||||||
},
|
},
|
||||||
"./*": "./*"
|
"./*": "./*"
|
||||||
},
|
},
|
||||||
"version": "6.16.3",
|
"version": "6.17.0",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"imports": {
|
"imports": {
|
||||||
"#wasm-engine-loader": {
|
"#wasm-engine-loader": {
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/generated/prisma/runtime/library.d.ts
vendored
1
src/generated/prisma/runtime/library.d.ts
vendored
@ -3062,6 +3062,7 @@ declare type QueryPlanNode = {
|
|||||||
args: {
|
args: {
|
||||||
from: QueryPlanNode;
|
from: QueryPlanNode;
|
||||||
to: QueryPlanNode;
|
to: QueryPlanNode;
|
||||||
|
fields: string[];
|
||||||
};
|
};
|
||||||
} | {
|
} | {
|
||||||
type: 'initializeRecord';
|
type: 'initializeRecord';
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
10
src/generated/prisma/runtime/react-native.js
vendored
10
src/generated/prisma/runtime/react-native.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -35,12 +35,12 @@ exports.Prisma = Prisma
|
|||||||
exports.$Enums = {}
|
exports.$Enums = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prisma Client JS version: 6.16.3
|
* Prisma Client JS version: 6.17.0
|
||||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||||
*/
|
*/
|
||||||
Prisma.prismaVersion = {
|
Prisma.prismaVersion = {
|
||||||
client: "6.16.3",
|
client: "6.17.0",
|
||||||
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
|
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
|
||||||
}
|
}
|
||||||
|
|
||||||
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
||||||
@ -412,7 +412,7 @@ const config = {
|
|||||||
"value": "prisma-client-js"
|
"value": "prisma-client-js"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||||
"fromEnvVar": null
|
"fromEnvVar": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@ -426,7 +426,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"previewFeatures": [],
|
"previewFeatures": [],
|
||||||
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
@ -434,8 +434,8 @@ const config = {
|
|||||||
"schemaEnvPath": "../../../.env"
|
"schemaEnvPath": "../../../.env"
|
||||||
},
|
},
|
||||||
"relativePath": "../../../prisma",
|
"relativePath": "../../../prisma",
|
||||||
"clientVersion": "6.16.3",
|
"clientVersion": "6.17.0",
|
||||||
"engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||||
"datasourceNames": [
|
"datasourceNames": [
|
||||||
"db"
|
"db"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { reloadTeam } from '@/lib/sse-actions'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useTeamStore } from '@/lib/stores'
|
import { useTeamStore } from '@/lib/stores'
|
||||||
import { TEAM_EVENTS, SELF_EVENTS, SSEEventType } from '@/lib/sseEvents'
|
import { TEAM_EVENTS, SELF_EVENTS, SSEEventType } from '@/lib/sseEvents'
|
||||||
|
import { useGameBannerStore } from '@/lib/useGameBannerStore' // ← NEU
|
||||||
|
|
||||||
export default function SSEHandler() {
|
export default function SSEHandler() {
|
||||||
const { data: session, status } = useSession()
|
const { data: session, status } = useSession()
|
||||||
@ -15,16 +16,21 @@ export default function SSEHandler() {
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { setTeam, team } = useTeamStore()
|
const { setTeam, team } = useTeamStore()
|
||||||
const { connect, disconnect, lastEvent, source } = useSSEStore()
|
const { connect, source, lastEvent } = useSSEStore()
|
||||||
|
|
||||||
|
// Banner-Store
|
||||||
|
const setBanner = useGameBannerStore(s => s.setGameBanner) // ← NEU
|
||||||
|
const patchBanner = useGameBannerStore(s => s.patchGameBanner) // ← NEU
|
||||||
|
|
||||||
|
// Verbindung (pro User/Gast nur 1x)
|
||||||
const prevSteamId = useRef<string | null>(null)
|
const prevSteamId = useRef<string | null>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = (status === 'authenticated' && steamId) ? steamId : 'guest'; // <- neu
|
const id = (status === 'authenticated' && steamId) ? steamId : 'guest'
|
||||||
if (!source || prevSteamId.current !== id) {
|
if (!source || prevSteamId.current !== id) {
|
||||||
connect(id);
|
connect(id)
|
||||||
prevSteamId.current = id;
|
prevSteamId.current = id
|
||||||
}
|
}
|
||||||
}, [status, steamId, connect, source]);
|
}, [status, steamId, connect, source])
|
||||||
|
|
||||||
// parallele Reloads pro Team vermeiden
|
// parallele Reloads pro Team vermeiden
|
||||||
const reloadInFlight = useRef<Set<string>>(new Set())
|
const reloadInFlight = useRef<Set<string>>(new Set())
|
||||||
@ -37,20 +43,19 @@ export default function SSEHandler() {
|
|||||||
if (ts && ts <= lastHandledTs.current) return
|
if (ts && ts <= lastHandledTs.current) return
|
||||||
lastHandledTs.current = ts ?? Date.now()
|
lastHandledTs.current = ts ?? Date.now()
|
||||||
|
|
||||||
const teamId: string | undefined = payload?.teamId
|
// Robust: Daten können top-level ODER in payload liegen
|
||||||
|
const data: any = payload ?? lastEvent
|
||||||
|
|
||||||
|
const teamId: string | undefined = data?.teamId
|
||||||
|
|
||||||
const reloadIfNeeded = async (tid?: string) => {
|
const reloadIfNeeded = async (tid?: string) => {
|
||||||
if (!tid) return
|
if (!tid) return
|
||||||
// nur reloaden, wenn es auch das aktuell angezeigte Team ist
|
|
||||||
if (team?.id && tid !== team.id) return
|
if (team?.id && tid !== team.id) return
|
||||||
if (reloadInFlight.current.has(tid)) return
|
if (reloadInFlight.current.has(tid)) return
|
||||||
reloadInFlight.current.add(tid)
|
reloadInFlight.current.add(tid)
|
||||||
try {
|
try {
|
||||||
const updated = await reloadTeam(tid)
|
const updated = await reloadTeam(tid)
|
||||||
if (updated) {
|
if (updated) startTransition(() => setTeam(updated))
|
||||||
// nicht render-blockierend updaten
|
|
||||||
startTransition(() => setTeam(updated))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[SSE] reloadTeam failed:', e)
|
console.error('[SSE] reloadTeam failed:', e)
|
||||||
} finally {
|
} finally {
|
||||||
@ -59,15 +64,50 @@ export default function SSEHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSelfExit = () => {
|
const handleSelfExit = () => {
|
||||||
// nur handeln, wenn wir noch ein Team im Store haben
|
|
||||||
if (team) {
|
if (team) {
|
||||||
startTransition(() => setTeam(null as any))
|
startTransition(() => setTeam(null as any))
|
||||||
// replace statt push, um History nicht zu fluten
|
|
||||||
router.replace('/team')
|
router.replace('/team')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
;(async () => {
|
||||||
|
// ---------- Banner-Events (NEU) ----------
|
||||||
|
if (type === 'match-ready') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'server-config-updated') {
|
||||||
|
const activeMatchId = data?.activeMatchId ?? data?.payload?.activeMatchId
|
||||||
|
if (activeMatchId) {
|
||||||
|
setBanner({
|
||||||
|
visible: true,
|
||||||
|
variant: 'connected',
|
||||||
|
serverLabel: 'CS2 Server',
|
||||||
|
mapKey: data?.activeMapKey ?? data?.payload?.activeMapKey,
|
||||||
|
mapLabel: data?.activeMapLabel ?? data?.payload?.activeMapLabel,
|
||||||
|
bgUrl: data?.activeMapBg ?? data?.payload?.activeMapBg,
|
||||||
|
connectUri: 'steam://rungameid/730',
|
||||||
|
totalExpected: (data?.activeParticipants ?? data?.payload?.activeParticipants)?.length ?? 10,
|
||||||
|
connectedCount: 0,
|
||||||
|
missingCount: 0,
|
||||||
|
phase: 'warmup',
|
||||||
|
score: '– : –',
|
||||||
|
zIndex: 9999,
|
||||||
|
inline: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Kein aktives Match -> Banner aus
|
||||||
|
setBanner(null)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'match-ended' || type === 'server-reset') {
|
||||||
|
setBanner(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
// 1) Team-relevante Events → evtl. Reload
|
// 1) Team-relevante Events → evtl. Reload
|
||||||
if (TEAM_EVENTS.has(type as SSEEventType)) {
|
if (TEAM_EVENTS.has(type as SSEEventType)) {
|
||||||
await reloadIfNeeded(teamId)
|
await reloadIfNeeded(teamId)
|
||||||
@ -80,11 +120,10 @@ export default function SSEHandler() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) alles andere (Notifications etc.) hier ignorieren
|
// 3) alles andere ignorieren (z.B. Notifications → eigene Komponente)
|
||||||
// -> werden in anderen Komponenten ausgewertet
|
|
||||||
})()
|
})()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [lastEvent, team?.id, setTeam, router])
|
}, [lastEvent, team?.id, setTeam, router, setBanner, patchBanner])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,12 @@ export const SSE_EVENT_TYPES = [
|
|||||||
'match-ready',
|
'match-ready',
|
||||||
'map-vote-reset',
|
'map-vote-reset',
|
||||||
'ready-updated',
|
'ready-updated',
|
||||||
|
'server-config-updated',
|
||||||
|
'match-ended',
|
||||||
|
'server-reset',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|
||||||
export type SSEEventType = typeof SSE_EVENT_TYPES[number];
|
export type SSEEventType = typeof SSE_EVENT_TYPES[number];
|
||||||
|
|
||||||
/** Type Guard */
|
/** Type Guard */
|
||||||
@ -66,6 +70,14 @@ export const INVITE_EVENTS = makeEventSet([
|
|||||||
'team-invite-revoked',
|
'team-invite-revoked',
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
|
||||||
|
export const MAPVOTE_REFRESH = makeEventSet([
|
||||||
|
'map-vote-updated',
|
||||||
|
'map-vote-reset',
|
||||||
|
'map-vote-admin-edit',
|
||||||
|
'team-leader-changed'
|
||||||
|
] as const);
|
||||||
|
|
||||||
export const SELF_EVENTS = makeEventSet([
|
export const SELF_EVENTS = makeEventSet([
|
||||||
'self-updated',
|
'self-updated',
|
||||||
'team-created',
|
'team-created',
|
||||||
@ -86,8 +98,12 @@ export const MATCH_EVENTS = makeEventSet([
|
|||||||
'match-ready',
|
'match-ready',
|
||||||
'map-vote-reset',
|
'map-vote-reset',
|
||||||
'ready-updated',
|
'ready-updated',
|
||||||
|
'server-config-updated',
|
||||||
|
'match-ended',
|
||||||
|
'server-reset',
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
|
||||||
// Events, die das NotificationCenter betreffen
|
// Events, die das NotificationCenter betreffen
|
||||||
export const NOTIFICATION_EVENTS = makeEventSet([
|
export const NOTIFICATION_EVENTS = makeEventSet([
|
||||||
'notification',
|
'notification',
|
||||||
@ -100,6 +116,14 @@ export const NOTIFICATION_EVENTS = makeEventSet([
|
|||||||
'expired-sharecode',
|
'expired-sharecode',
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
|
||||||
|
export const BANNER_EVENTS = makeEventSet([
|
||||||
|
'match-ready',
|
||||||
|
'server-config-updated',
|
||||||
|
'match-ended',
|
||||||
|
'server-reset',
|
||||||
|
] as const);
|
||||||
|
|
||||||
/** Nur noch: akzeptiere kanonische Typen, sonst null */
|
/** Nur noch: akzeptiere kanonische Typen, sonst null */
|
||||||
export function normalizeEventType(incoming: string): SSEEventType | null {
|
export function normalizeEventType(incoming: string): SSEEventType | null {
|
||||||
return isSseEventType(incoming) ? incoming : null;
|
return isSseEventType(incoming) ? incoming : null;
|
||||||
|
|||||||
@ -1,18 +1,48 @@
|
|||||||
|
// /src/lib/useGameBannerStore.ts
|
||||||
'use client'
|
'use client'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
|
||||||
type BannerState = {
|
export type GameBannerData = {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
map: { key: string|null, label: string|null, bg: string|null }
|
variant: 'connected' | 'disconnected'
|
||||||
participants: string[]
|
serverLabel?: string
|
||||||
show: (p: { key: string|null, label: string|null, bg: string|null, participants: string[] }) => void
|
mapKey?: string
|
||||||
hide: () => void
|
mapLabel?: string
|
||||||
|
bgUrl?: string
|
||||||
|
iconUrl?: string
|
||||||
|
connectUri?: string
|
||||||
|
totalExpected?: number
|
||||||
|
connectedCount?: number
|
||||||
|
missingCount?: number
|
||||||
|
phase?: string
|
||||||
|
score?: string
|
||||||
|
zIndex?: number
|
||||||
|
inline?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGameBannerStore = create<BannerState>((set) => ({
|
type GameBannerState = {
|
||||||
visible: false,
|
gameBanner: GameBannerData | null
|
||||||
map: { key: null, label: null, bg: null },
|
gameBannerPx: number
|
||||||
participants: [],
|
setGameBanner: (data: GameBannerData | null) => void
|
||||||
show: ({ key, label, bg, participants }) => set({ visible: true, map: { key, label, bg }, participants }),
|
patchGameBanner: (patch: Partial<GameBannerData>) => void
|
||||||
hide: () => set({ visible: false, participants: [], map: { key: null, label: null, bg: null } }),
|
setGameBannerPx: (px: number) => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGameBannerStore = create<GameBannerState>((set, get) => ({
|
||||||
|
gameBanner: null,
|
||||||
|
gameBannerPx: 0,
|
||||||
|
|
||||||
|
setGameBanner: (data) => set({ gameBanner: data }),
|
||||||
|
|
||||||
|
patchGameBanner: (patch) => {
|
||||||
|
const cur = get().gameBanner
|
||||||
|
if (!cur) return
|
||||||
|
set({ gameBanner: { ...cur, ...patch } })
|
||||||
|
},
|
||||||
|
|
||||||
|
setGameBannerPx: (px) =>
|
||||||
|
set({ gameBannerPx: Math.max(0, Math.floor(px)) }),
|
||||||
|
|
||||||
|
reset: () => set({ gameBanner: null, gameBannerPx: 0 }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { create } from 'zustand'
|
|
||||||
|
|
||||||
type UiChromeState = {
|
|
||||||
gameBannerPx: number // aktuelle Bannerhöhe in Pixel (0 wenn unsichtbar)
|
|
||||||
setGameBannerPx: (px: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useUiChromeStore = create<UiChromeState>((set) => ({
|
|
||||||
gameBannerPx: 0,
|
|
||||||
setGameBannerPx: (px) => set({ gameBannerPx: Math.max(0, Math.floor(px)) }),
|
|
||||||
}))
|
|
||||||
Loading…
x
Reference in New Issue
Block a user