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",
|
||||
"@preline/dropdown": "^3.0.1",
|
||||
"@preline/tooltip": "^3.0.0",
|
||||
"@prisma/client": "^6.16.3",
|
||||
"@prisma/client": "^6.17.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"csgo-sharecode": "^3.1.2",
|
||||
@ -60,7 +60,7 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.0",
|
||||
"prisma": "^6.16.3",
|
||||
"prisma": "^6.17.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.19.4",
|
||||
@ -85,6 +85,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@ -122,7 +123,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@ -1577,6 +1577,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@ -1597,9 +1598,9 @@
|
||||
"license": "Licensed under MIT and Preline UI Fair Use License"
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.3.tgz",
|
||||
"integrity": "sha512-JfNfAtXG+/lIopsvoZlZiH2k5yNx87mcTS4t9/S5oufM1nKdXYxOvpDC1XoTCFBa5cQh7uXnbMPsmZrwZY80xw==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.0.tgz",
|
||||
"integrity": "sha512-b42mTLOdLEZ6e/igu8CLdccAUX9AwHknQQ1+pHOftnzDP2QoyZyFvcANqSLs5ockimFKJnV7Ljf+qrhNYf6oAg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@ -1619,9 +1620,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config": {
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.3.tgz",
|
||||
"integrity": "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.0.tgz",
|
||||
"integrity": "sha512-k8tuChKpkO/Vj7ZEzaQMNflNGbaW4X0r8+PC+W2JaqVRdiS2+ORSv1SrDwNxsb8YyzIQJucXqLGZbgxD97ZhsQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@ -1632,53 +1633,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.3.tgz",
|
||||
"integrity": "sha512-89DdqWtdKd7qoc9/qJCKLTazj3W3zPEiz0hc7HfZdpjzm21c7orOUB5oHWJsG+4KbV4cWU5pefq3CuDVYF9vgA==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.0.tgz",
|
||||
"integrity": "sha512-eE2CB32nr1hRqyLVnOAVY6c//iSJ/PN+Yfoa/2sEzLGpORaCg61d+nvdAkYSh+6Y2B8L4BVyzkRMANLD6nnC2g==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.3.tgz",
|
||||
"integrity": "sha512-b+Rl4nzQDcoqe6RIpSHv8f5lLnwdDGvXhHjGDiokObguAAv/O1KaX1Oc69mBW/GFWKQpCkOraobLjU6s1h8HGg==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.0.tgz",
|
||||
"integrity": "sha512-XhE9v3hDQTNgCYMjogcCYKi7HCEkZf9WwTGuXy8cmY8JUijvU0ap4M7pGLx4pBblkp5EwUsYzw1YLtH7yi0GZw==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.16.3",
|
||||
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"@prisma/fetch-engine": "6.16.3",
|
||||
"@prisma/get-platform": "6.16.3"
|
||||
"@prisma/debug": "6.17.0",
|
||||
"@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||
"@prisma/fetch-engine": "6.17.0",
|
||||
"@prisma/get-platform": "6.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a.tgz",
|
||||
"integrity": "sha512-fftRmosBex48Ph1v2ll1FrPpirwtPZpNkE5CDCY1Lw2SD2ctyrLlVlHiuxDAAlALwWBOkPbAll4+EaqdGuMhJw==",
|
||||
"version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a.tgz",
|
||||
"integrity": "sha512-G0VU4uFDreATgTz4sh3dTtU2C+jn+J6c060ixavWZaUaSRZsNQhSPW26lbfez7GHzR02RGCdqs5UcSuGBC3yLw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.3.tgz",
|
||||
"integrity": "sha512-bUoRIkVaI+CCaVGrSfcKev0/Mk4ateubqWqGZvQ9uCqFv2ENwWIR3OeNuGin96nZn5+SkebcD7RGgKr/+mJelw==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.0.tgz",
|
||||
"integrity": "sha512-YSl5R3WIAPrmshYPkaaszOsBIWRAovOCHn3y7gkTNGG51LjYW4pi6PFNkGouW6CA06qeTjTbGrDRCgFjnmVWDg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.16.3",
|
||||
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"@prisma/get-platform": "6.16.3"
|
||||
"@prisma/debug": "6.17.0",
|
||||
"@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||
"@prisma/get-platform": "6.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.3.tgz",
|
||||
"integrity": "sha512-X1LxiFXinJ4iQehrodGp0f66Dv6cDL0GbRlcCoLtSu6f4Wi+hgo7eND/afIs5029GQLgNWKZ46vn8hjyXTsHLA==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.0.tgz",
|
||||
"integrity": "sha512-3tEKChrnlmLXPd870oiVfRvj7vVKuxqP349hYaMDsbV4TZd3+lFqw8KTI2Tbq5DopamfNuNqhVCj+R6ZxKKYGQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.16.3"
|
||||
"@prisma/debug": "6.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
@ -2067,7 +2068,6 @@
|
||||
"integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
@ -2085,7 +2085,6 @@
|
||||
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@ -2183,7 +2182,6 @@
|
||||
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.30.1",
|
||||
"@typescript-eslint/types": "8.30.1",
|
||||
@ -2617,7 +2615,6 @@
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -3286,6 +3283,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -3426,7 +3424,6 @@
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
@ -3883,7 +3880,6 @@
|
||||
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@ -4058,7 +4054,6 @@
|
||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.8",
|
||||
@ -5407,6 +5402,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@ -5835,6 +5831,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
@ -6022,7 +6019,6 @@
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.0.tgz",
|
||||
"integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@next/env": "15.3.0",
|
||||
"@swc/counter": "0.1.3",
|
||||
@ -6315,7 +6311,8 @@
|
||||
"version": "0.9.15",
|
||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -6332,6 +6329,7 @@
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
@ -6460,6 +6458,7 @@
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
|
||||
"integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"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",
|
||||
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pretty-format": "^3.8.0"
|
||||
},
|
||||
@ -6776,19 +6776,19 @@
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.3.tgz",
|
||||
"integrity": "sha512-4tJq3KB9WRshH5+QmzOLV54YMkNlKOtLKaSdvraI5kC/axF47HuOw6zDM8xrxJ6s9o2WodY654On4XKkrobQdQ==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.0.tgz",
|
||||
"integrity": "sha512-rcvldz98r+2bVCs0MldQCBaaVJRCj9Ew4IqphLATF89OJcSzwRQpwnKXR+W2+2VjK7/o2x3ffu5+2N3Muu6Dbw==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.16.3",
|
||||
"@prisma/engines": "6.16.3"
|
||||
"@prisma/config": "6.17.0",
|
||||
"@prisma/engines": "6.17.0"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
@ -6902,7 +6902,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -6912,7 +6911,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@ -6982,7 +6980,8 @@
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.4",
|
||||
@ -7639,8 +7638,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
||||
"integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
@ -7697,7 +7695,6 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -7923,7 +7920,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -8179,7 +8175,8 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||
"@preline/dropdown": "^3.0.1",
|
||||
"@preline/tooltip": "^3.0.0",
|
||||
"@prisma/client": "^6.16.3",
|
||||
"@prisma/client": "^6.17.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"csgo-sharecode": "^3.1.2",
|
||||
@ -66,7 +66,7 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.0",
|
||||
"prisma": "^6.16.3",
|
||||
"prisma": "^6.17.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.19.4",
|
||||
|
||||
@ -11,7 +11,6 @@ export default function AdminPage() {
|
||||
const activeTab: 'teams' | 'matches' | 'privacy' | '' =
|
||||
pathname.startsWith('/admin/teams') ? 'teams' :
|
||||
pathname.startsWith('/admin/matches') ? 'matches' :
|
||||
pathname.startsWith('/admin/privacy') ? 'privacy' :
|
||||
''
|
||||
|
||||
switch (activeTab) {
|
||||
@ -22,12 +21,6 @@ export default function AdminPage() {
|
||||
</Card>
|
||||
)
|
||||
|
||||
case 'privacy':
|
||||
return (
|
||||
<Card title="Datenschutz"
|
||||
description="Einstellungen zum Schutz deiner Daten." />
|
||||
)
|
||||
|
||||
case 'teams':
|
||||
return (
|
||||
<Card title="Teams"
|
||||
|
||||
@ -6,7 +6,6 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
<div className="container mx-auto">
|
||||
<Tabs>
|
||||
<Tab name="Spielpläne" href="/admin/matches" />
|
||||
<Tab name="Privacy" href="/admin/privacy" />
|
||||
<Tab name="Teams" href="/admin/teams" />
|
||||
<Tab name="Serververwaltung" href="/admin/server" />
|
||||
</Tabs>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
// src/app/components/GameBanner.tsx
|
||||
// src/app/[locale]/components/GameBanner.tsx
|
||||
'use client'
|
||||
|
||||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import Link from 'next/link'
|
||||
import Button from './Button'
|
||||
import {useUiChromeStore} from '@/lib/useUiChromeStore'
|
||||
import { useGameBannerStore } from '@/lib/useGameBannerStore'
|
||||
import {MAP_OPTIONS} from '@/lib/mapOptions'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
@ -26,6 +26,8 @@ type Props = {
|
||||
inline?: boolean
|
||||
connectUri?: string
|
||||
missingCount?: number
|
||||
bgUrl?: string
|
||||
iconUrl?: string
|
||||
}
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
@ -70,7 +72,7 @@ export default function GameBanner(props: Props) {
|
||||
} = props
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
const setBannerPx = useUiChromeStore(s => s.setGameBannerPx)
|
||||
const setBannerPx = useGameBannerStore(s => s.setGameBannerPx)
|
||||
const isSmDown = useIsSmDown()
|
||||
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-amber-700/95 text-white ring-1 ring-black/10'
|
||||
|
||||
const bgUrl = pickMapImageFromOptions(mapKey)
|
||||
const iconUrl = isConnected ? (pickMapIcon(mapKey) ?? '') : '/assets/img/icons/ui/disconnect.svg'
|
||||
const bgUrl = props.bgUrl ?? pickMapImageFromOptions(mapKey)
|
||||
const iconUrl = props.iconUrl ?? (isConnected ? (pickMapIcon(mapKey) ?? '') : '/assets/img/icons/ui/disconnect.svg')
|
||||
|
||||
|
||||
const pretty = {
|
||||
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'
|
||||
import {useUiChromeStore} from '@/lib/useUiChromeStore'
|
||||
import { useGameBannerStore } from '@/lib/useGameBannerStore'
|
||||
|
||||
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 />
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import type { MapVoteState } from '../../../types/mapvote'
|
||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||
import { Tabs } from './Tabs'
|
||||
import Chart from './Chart'
|
||||
import { MAPVOTE_REFRESH } from '@/lib/sseEvents'
|
||||
|
||||
/* =================== Utilities & constants =================== */
|
||||
|
||||
@ -189,32 +190,6 @@ export default function MapVotePanel({ match }: Props) {
|
||||
|
||||
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 -------- */
|
||||
const adminEditingBy = state?.adminEdit?.by ?? null
|
||||
const adminEditingEnabled = !!state?.adminEdit?.enabled
|
||||
@ -227,8 +202,10 @@ export default function MapVotePanel({ match }: Props) {
|
||||
const me = session?.user
|
||||
const isAdmin = !!me?.isAdmin
|
||||
const mySteamId = me?.steamId
|
||||
const isLeaderA = !!mySteamId && match.teamA?.leader?.steamId === mySteamId
|
||||
const isLeaderB = !!mySteamId && match.teamB?.leader?.steamId === mySteamId
|
||||
const leaderAId = state?.teams?.teamA?.leader?.steamId ?? match.teamA?.leader?.steamId ?? null
|
||||
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
|
||||
|
||||
@ -457,6 +434,62 @@ export default function MapVotePanel({ match }: Props) {
|
||||
[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 amInTeamB = !!mySteamId && playersB.some(p => p.user?.steamId === mySteamId)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
if (tab !== 'winrate') return;
|
||||
|
||||
@ -25,6 +25,7 @@ import Link from 'next/link'
|
||||
import Card from './Card'
|
||||
import MiniPlayerCard from './MiniPlayerCard'
|
||||
import type { PlayerSummary } from './MiniPlayerCard'
|
||||
import UserAvatarWithStatus from './UserAvatarWithStatus'
|
||||
|
||||
// Für den Prefetch greifen wir dieselbe Performance-Logik auf:
|
||||
const KD_CAP = 2.0
|
||||
@ -623,7 +624,8 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
players: MatchPlayer[],
|
||||
teamTitle: string,
|
||||
showEdit: boolean,
|
||||
onEditClick?: () => void
|
||||
onEditClick?: () => void,
|
||||
leaderSteamId?: string | null,
|
||||
) => {
|
||||
const sorted = [...players].sort(
|
||||
(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) => {
|
||||
const banned = isPlayerBanned(p);
|
||||
const title = banned ? banTooltip(p) : undefined;
|
||||
const isLeader = p.user.steamId === leaderSteamId // ⬅️ hier prüfen
|
||||
|
||||
return (
|
||||
<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}`)}
|
||||
>
|
||||
<img
|
||||
<UserAvatarWithStatus
|
||||
steamId={p.user.steamId}
|
||||
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
||||
alt={p.user.name}
|
||||
size={36}
|
||||
isLeader={isLeader}
|
||||
showStatus={false}
|
||||
className="mr-3 h-8 w-8 rounded-full"
|
||||
alignRight={false}
|
||||
/>
|
||||
<div className="text-base font-semibold flex items-center gap-2">
|
||||
<span>{p.user.name ?? 'Unbekannt'}</span>
|
||||
@ -963,12 +971,12 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
{/* Teams / Tabellen */}
|
||||
{/* Team A */}
|
||||
<div>
|
||||
{renderTable(teamAPlayers, teamATitle, showEditA, () => setEditSide('A'))}
|
||||
{renderTable(teamAPlayers, teamATitle, showEditA, () => setEditSide('A'), match.teamA?.leader?.steamId)}
|
||||
</div>
|
||||
|
||||
{/* Team B */}
|
||||
<div>
|
||||
{renderTable(teamBPlayers, teamBTitle, showEditB, () => setEditSide('B'))}
|
||||
{renderTable(teamBPlayers, teamBTitle, showEditB, () => setEditSide('B'), match.teamB?.leader?.steamId)}
|
||||
</div>
|
||||
|
||||
{/* Echte Modals (außerhalb IIFE, State oben): */}
|
||||
|
||||
@ -79,6 +79,7 @@ export default function MiniCard({
|
||||
const cardClasses = `
|
||||
relative flex flex-col items-center p-4 border rounded-lg transition
|
||||
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}
|
||||
${hoverEffect ? 'hover:cursor-grab hover:scale-105' : ''}
|
||||
${isSelectable ? 'cursor-pointer' : ''}
|
||||
|
||||
@ -8,7 +8,7 @@ import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSSEStore } from '@/lib/useSSEStore'
|
||||
import { NOTIFICATION_EVENTS, isSseEventType } from '@/lib/sseEvents'
|
||||
import { useUiChromeStore } from '@/lib/useUiChromeStore'
|
||||
import { useGameBannerStore } from '@/lib/useGameBannerStore'
|
||||
|
||||
type Notification = {
|
||||
id: string
|
||||
@ -39,7 +39,7 @@ export default function NotificationBell() {
|
||||
const router = useRouter()
|
||||
const { lastEvent } = useSSEStore() // nur konsumieren, nicht connecten
|
||||
const bellRef = useRef<HTMLButtonElement | null>(null);
|
||||
const telemetryBannerPx = useUiChromeStore(s => s.gameBannerPx)
|
||||
const telemetryBannerPx = useGameBannerStore(s => s.gameBannerPx)
|
||||
|
||||
const [notifications, setNotifications] = useState<Notification[]>([])
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@ -156,20 +156,42 @@ export default function TelemetrySocket() {
|
||||
useEffect(() => {
|
||||
aliveRef.current = true
|
||||
|
||||
const connect = () => {
|
||||
let wsLocal: WebSocket | null = null // <- dieses WS schließen wir im Cleanup
|
||||
const connectOnce = () => {
|
||||
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)
|
||||
wsLocal = ws
|
||||
wsRef.current = ws
|
||||
|
||||
ws.onopen = () => {
|
||||
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 = () => {
|
||||
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] error')
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
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) => {
|
||||
@ -241,13 +263,13 @@ export default function TelemetrySocket() {
|
||||
}
|
||||
}
|
||||
|
||||
connect()
|
||||
connectOnce()
|
||||
return () => {
|
||||
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 {}
|
||||
}
|
||||
}, [url, setSnapshot, setJoin, setLeave, setMapKey, setPhase, hideOverlay, mySteamId])
|
||||
}, [url])
|
||||
|
||||
|
||||
// Wenn die API { matchId: null } liefert → KEIN Banner
|
||||
|
||||
@ -5,10 +5,11 @@ import { useEffect, useState, useMemo } from 'react'
|
||||
import Image from 'next/image'
|
||||
import clsx from 'clsx'
|
||||
import { useSSEStore } from '@/lib/useSSEStore'
|
||||
import type React from 'react'
|
||||
|
||||
type Presence = 'online' | 'away' | 'offline'
|
||||
|
||||
type Props = {
|
||||
type Props = React.HTMLAttributes<HTMLDivElement> & {
|
||||
steamId?: string | null
|
||||
src: string
|
||||
alt?: string
|
||||
@ -17,6 +18,8 @@ type Props = {
|
||||
isLeader?: boolean
|
||||
alignRight?: boolean
|
||||
showStatus?: boolean
|
||||
/** Zusätzliche Klassen für den runden Avatar-/Ring-Container */
|
||||
avatarClassName?: string
|
||||
}
|
||||
|
||||
export default function UserAvatarWithStatus({
|
||||
@ -28,21 +31,20 @@ export default function UserAvatarWithStatus({
|
||||
isLeader = false,
|
||||
alignRight = false,
|
||||
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) {
|
||||
const { lastEvent } = useSSEStore()
|
||||
const [status, setStatus] = useState<Presence>(initialStatus)
|
||||
|
||||
// Dynamik für Krone (Skalierung & Offset relativ zu size)
|
||||
const { crownSize, crownOffset, crownIconSize } = useMemo(() => {
|
||||
// Button-Durchmesser max. 20px
|
||||
const cs = Math.min(20, Math.round(Math.max(size * 0.5, 14)))
|
||||
const off = Math.round(size * 0.10) // Offset bleibt dynamisch
|
||||
// Icon etwas kleiner (z.B. 70% vom Button)
|
||||
const off = Math.round(size * 0.10)
|
||||
const icon = Math.round(cs * 0.70)
|
||||
return { crownSize: cs, crownOffset: off, crownIconSize: icon }
|
||||
}, [size])
|
||||
|
||||
// initialer Status
|
||||
useEffect(() => {
|
||||
if (!showStatus || !steamId) return
|
||||
let alive = true
|
||||
@ -57,15 +59,12 @@ export default function UserAvatarWithStatus({
|
||||
return () => { alive = false }
|
||||
}, [steamId, showStatus])
|
||||
|
||||
// SSE-Updates
|
||||
useEffect(() => {
|
||||
if (!showStatus || !steamId) return
|
||||
if (!lastEvent || lastEvent.type !== 'user-status-updated') return
|
||||
|
||||
const raw = lastEvent.payload as any
|
||||
const sid = raw?.steamId ?? raw?.payload?.steamId
|
||||
const st = (raw?.status ?? raw?.payload?.status) as Presence | undefined
|
||||
|
||||
if (sid === steamId && st) setStatus(st)
|
||||
}, [lastEvent, steamId, showStatus])
|
||||
|
||||
@ -78,15 +77,17 @@ export default function UserAvatarWithStatus({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative items-center justify-center"
|
||||
className={clsx('relative inline-flex items-center justify-center', className)}
|
||||
style={{ width: size, height: size }}
|
||||
{...rest}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-center rounded-full transition',
|
||||
showStatus
|
||||
? ['ring-1 ring-offset-1 ring-offset-white dark:ring-offset-neutral-900', statusRing]
|
||||
: null
|
||||
: null,
|
||||
avatarClassName
|
||||
)}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
@ -99,7 +100,6 @@ export default function UserAvatarWithStatus({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Team-Leader-Krone – skaliert & positioniert relativ zur Avatargröße */}
|
||||
{isLeader && (
|
||||
<span
|
||||
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">
|
||||
<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>
|
||||
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -19,6 +19,7 @@ import AudioPrimer from './components/AudioPrimer';
|
||||
import ReadyOverlayHost from './components/ReadyOverlayHost';
|
||||
import TelemetrySocket from './components/TelemetrySocket';
|
||||
import GameBannerSpacer from './components/GameBannerSpacer';
|
||||
import GameBannerHost from './components/GameBannerHost';
|
||||
|
||||
const geistSans = Geist({variable: '--font-geist-sans', 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">
|
||||
<div className="h-full min-h-0 box-border p-4 sm:p-6">{children}</div>
|
||||
</main>
|
||||
<GameBannerHost />
|
||||
<GameBannerSpacer className="hidden sm:block" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@ import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { MapVoteAction } from '@/generated/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
import { randomInt, createHash, randomBytes } from 'crypto'
|
||||
import { randomInt, createHash } from 'crypto'
|
||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
@ -469,7 +469,7 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
||||
if (!sLike.locked) return
|
||||
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)
|
||||
if (chosen.length < bestOf) return
|
||||
if (chosen.length < bestOf) return false
|
||||
|
||||
// ⬇️ JSON bauen (enthält cs2MatchId/rndId)
|
||||
const json = buildMatchJson(mLike, sLike)
|
||||
@ -511,8 +511,6 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
||||
// … dann das neue laden
|
||||
await sendServerCommand(`matchzy_loadmatch ${filename}`)
|
||||
|
||||
// Spieler persistieren + cs2MatchId speichern wie gehabt
|
||||
await persistMatchPlayers(match)
|
||||
if (typeof json.matchid === 'number') {
|
||||
await prisma.match.update({
|
||||
where: { id: match.id },
|
||||
@ -524,8 +522,11 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
||||
data: { exportedAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (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),
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: 'server-config-updated',
|
||||
payload: {
|
||||
activeMatchId: match.id,
|
||||
activeMapKey: key,
|
||||
activeMapLabel: label,
|
||||
activeMapBg: bg,
|
||||
activeParticipants: participants,
|
||||
},
|
||||
})
|
||||
} catch (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 */
|
||||
async function afterVoteLocked(match: any, vote: any, mapVisuals: Record<string, any>) {
|
||||
// 1) Spieler für dieses Match festschreiben
|
||||
// 1) Spieler festschreiben
|
||||
await persistMatchPlayers(match)
|
||||
// 2) Live-State für Banner speichern
|
||||
await writeLiveStateToServerConfig(match, vote, mapVisuals)
|
||||
// 3) Serverexport + Matchzy-Load
|
||||
await exportMatchToSftpDirect(match, vote)
|
||||
|
||||
// 2) Serverexport + Matchzy-Load
|
||||
const ok = await exportMatchToSftpDirect(match, vote) // ← wartet bis Load versucht wurde
|
||||
|
||||
// 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 ---------- */
|
||||
|
||||
function deriveChosenSteps(vote: any) {
|
||||
@ -697,9 +714,6 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
||||
});
|
||||
|
||||
await afterVoteLocked(match, updated, mapVisuals)
|
||||
|
||||
// Export serverseitig
|
||||
await exportMatchToSftpDirect(match, updated)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Export serverseitig
|
||||
await exportMatchToSftpDirect(match, updated)
|
||||
}
|
||||
|
||||
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 exportMatchToSftpDirect(match, updated)
|
||||
}
|
||||
|
||||
return NextResponse.json({ ...shapeState(updated, match), mapVisuals, teams })
|
||||
|
||||
@ -97,7 +97,6 @@ export async function POST(req: NextRequest) {
|
||||
})))
|
||||
await sendServerSSEMessage({
|
||||
type: 'team-leader-changed',
|
||||
targetUserIds: others,
|
||||
teamId,
|
||||
message: textForOthers,
|
||||
actionData: newLeaderSteamId,
|
||||
|
||||
@ -35,12 +35,12 @@ exports.Prisma = Prisma
|
||||
exports.$Enums = {}
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 6.16.3
|
||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
||||
* Prisma Client JS version: 6.17.0
|
||||
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||
*/
|
||||
Prisma.prismaVersion = {
|
||||
client: "6.16.3",
|
||||
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
|
||||
client: "6.17.0",
|
||||
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
|
||||
}
|
||||
|
||||
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
||||
@ -412,7 +412,7 @@ const config = {
|
||||
"value": "prisma-client-js"
|
||||
},
|
||||
"output": {
|
||||
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||
"fromEnvVar": null
|
||||
},
|
||||
"config": {
|
||||
@ -426,7 +426,7 @@ const config = {
|
||||
}
|
||||
],
|
||||
"previewFeatures": [],
|
||||
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||
"isCustomOutput": true
|
||||
},
|
||||
"relativeEnvPaths": {
|
||||
@ -434,8 +434,8 @@ const config = {
|
||||
"schemaEnvPath": "../../../.env"
|
||||
},
|
||||
"relativePath": "../../../prisma",
|
||||
"clientVersion": "6.16.3",
|
||||
"engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"clientVersion": "6.17.0",
|
||||
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||
"datasourceNames": [
|
||||
"db"
|
||||
],
|
||||
|
||||
@ -20,12 +20,12 @@ exports.Prisma = Prisma
|
||||
exports.$Enums = {}
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 6.16.3
|
||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
||||
* Prisma Client JS version: 6.17.0
|
||||
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||
*/
|
||||
Prisma.prismaVersion = {
|
||||
client: "6.16.3",
|
||||
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
|
||||
client: "6.17.0",
|
||||
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 6.16.3
|
||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
||||
* Prisma Client JS version: 6.17.0
|
||||
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||
*/
|
||||
export type PrismaVersion = {
|
||||
client: string
|
||||
|
||||
@ -35,12 +35,12 @@ exports.Prisma = Prisma
|
||||
exports.$Enums = {}
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 6.16.3
|
||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
||||
* Prisma Client JS version: 6.17.0
|
||||
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||
*/
|
||||
Prisma.prismaVersion = {
|
||||
client: "6.16.3",
|
||||
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
|
||||
client: "6.17.0",
|
||||
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
|
||||
}
|
||||
|
||||
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
||||
@ -413,7 +413,7 @@ const config = {
|
||||
"value": "prisma-client-js"
|
||||
},
|
||||
"output": {
|
||||
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||
"fromEnvVar": null
|
||||
},
|
||||
"config": {
|
||||
@ -427,7 +427,7 @@ const config = {
|
||||
}
|
||||
],
|
||||
"previewFeatures": [],
|
||||
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||
"isCustomOutput": true
|
||||
},
|
||||
"relativeEnvPaths": {
|
||||
@ -435,8 +435,8 @@ const config = {
|
||||
"schemaEnvPath": "../../../.env"
|
||||
},
|
||||
"relativePath": "../../../prisma",
|
||||
"clientVersion": "6.16.3",
|
||||
"engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"clientVersion": "6.17.0",
|
||||
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||
"datasourceNames": [
|
||||
"db"
|
||||
],
|
||||
|
||||
@ -151,7 +151,7 @@
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"version": "6.16.3",
|
||||
"version": "6.17.0",
|
||||
"sideEffects": false,
|
||||
"imports": {
|
||||
"#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: {
|
||||
from: QueryPlanNode;
|
||||
to: QueryPlanNode;
|
||||
fields: string[];
|
||||
};
|
||||
} | {
|
||||
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 = {}
|
||||
|
||||
/**
|
||||
* Prisma Client JS version: 6.16.3
|
||||
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
|
||||
* Prisma Client JS version: 6.17.0
|
||||
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
|
||||
*/
|
||||
Prisma.prismaVersion = {
|
||||
client: "6.16.3",
|
||||
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
|
||||
client: "6.17.0",
|
||||
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
|
||||
}
|
||||
|
||||
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
||||
@ -412,7 +412,7 @@ const config = {
|
||||
"value": "prisma-client-js"
|
||||
},
|
||||
"output": {
|
||||
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||
"fromEnvVar": null
|
||||
},
|
||||
"config": {
|
||||
@ -426,7 +426,7 @@ const config = {
|
||||
}
|
||||
],
|
||||
"previewFeatures": [],
|
||||
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||
"isCustomOutput": true
|
||||
},
|
||||
"relativeEnvPaths": {
|
||||
@ -434,8 +434,8 @@ const config = {
|
||||
"schemaEnvPath": "../../../.env"
|
||||
},
|
||||
"relativePath": "../../../prisma",
|
||||
"clientVersion": "6.16.3",
|
||||
"engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"clientVersion": "6.17.0",
|
||||
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
|
||||
"datasourceNames": [
|
||||
"db"
|
||||
],
|
||||
|
||||
@ -8,6 +8,7 @@ import { reloadTeam } from '@/lib/sse-actions'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTeamStore } from '@/lib/stores'
|
||||
import { TEAM_EVENTS, SELF_EVENTS, SSEEventType } from '@/lib/sseEvents'
|
||||
import { useGameBannerStore } from '@/lib/useGameBannerStore' // ← NEU
|
||||
|
||||
export default function SSEHandler() {
|
||||
const { data: session, status } = useSession()
|
||||
@ -15,16 +16,21 @@ export default function SSEHandler() {
|
||||
|
||||
const router = useRouter()
|
||||
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)
|
||||
useEffect(() => {
|
||||
const id = (status === 'authenticated' && steamId) ? steamId : 'guest'; // <- neu
|
||||
if (!source || prevSteamId.current !== id) {
|
||||
connect(id);
|
||||
prevSteamId.current = id;
|
||||
}
|
||||
}, [status, steamId, connect, source]);
|
||||
const id = (status === 'authenticated' && steamId) ? steamId : 'guest'
|
||||
if (!source || prevSteamId.current !== id) {
|
||||
connect(id)
|
||||
prevSteamId.current = id
|
||||
}
|
||||
}, [status, steamId, connect, source])
|
||||
|
||||
// parallele Reloads pro Team vermeiden
|
||||
const reloadInFlight = useRef<Set<string>>(new Set())
|
||||
@ -37,20 +43,19 @@ export default function SSEHandler() {
|
||||
if (ts && ts <= lastHandledTs.current) return
|
||||
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) => {
|
||||
if (!tid) return
|
||||
// nur reloaden, wenn es auch das aktuell angezeigte Team ist
|
||||
if (team?.id && tid !== team.id) return
|
||||
if (reloadInFlight.current.has(tid)) return
|
||||
reloadInFlight.current.add(tid)
|
||||
try {
|
||||
const updated = await reloadTeam(tid)
|
||||
if (updated) {
|
||||
// nicht render-blockierend updaten
|
||||
startTransition(() => setTeam(updated))
|
||||
}
|
||||
if (updated) startTransition(() => setTeam(updated))
|
||||
} catch (e) {
|
||||
console.error('[SSE] reloadTeam failed:', e)
|
||||
} finally {
|
||||
@ -59,15 +64,50 @@ export default function SSEHandler() {
|
||||
}
|
||||
|
||||
const handleSelfExit = () => {
|
||||
// nur handeln, wenn wir noch ein Team im Store haben
|
||||
if (team) {
|
||||
startTransition(() => setTeam(null as any))
|
||||
// replace statt push, um History nicht zu fluten
|
||||
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
|
||||
if (TEAM_EVENTS.has(type as SSEEventType)) {
|
||||
await reloadIfNeeded(teamId)
|
||||
@ -80,11 +120,10 @@ export default function SSEHandler() {
|
||||
return
|
||||
}
|
||||
|
||||
// 3) alles andere (Notifications etc.) hier ignorieren
|
||||
// -> werden in anderen Komponenten ausgewertet
|
||||
// 3) alles andere ignorieren (z.B. Notifications → eigene Komponente)
|
||||
})()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lastEvent, team?.id, setTeam, router])
|
||||
}, [lastEvent, team?.id, setTeam, router, setBanner, patchBanner])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@ -32,8 +32,12 @@ export const SSE_EVENT_TYPES = [
|
||||
'match-ready',
|
||||
'map-vote-reset',
|
||||
'ready-updated',
|
||||
'server-config-updated',
|
||||
'match-ended',
|
||||
'server-reset',
|
||||
] as const;
|
||||
|
||||
|
||||
export type SSEEventType = typeof SSE_EVENT_TYPES[number];
|
||||
|
||||
/** Type Guard */
|
||||
@ -66,6 +70,14 @@ export const INVITE_EVENTS = makeEventSet([
|
||||
'team-invite-revoked',
|
||||
] 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([
|
||||
'self-updated',
|
||||
'team-created',
|
||||
@ -86,8 +98,12 @@ export const MATCH_EVENTS = makeEventSet([
|
||||
'match-ready',
|
||||
'map-vote-reset',
|
||||
'ready-updated',
|
||||
'server-config-updated',
|
||||
'match-ended',
|
||||
'server-reset',
|
||||
] as const);
|
||||
|
||||
|
||||
// Events, die das NotificationCenter betreffen
|
||||
export const NOTIFICATION_EVENTS = makeEventSet([
|
||||
'notification',
|
||||
@ -100,6 +116,14 @@ export const NOTIFICATION_EVENTS = makeEventSet([
|
||||
'expired-sharecode',
|
||||
] 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 */
|
||||
export function normalizeEventType(incoming: string): SSEEventType | null {
|
||||
return isSseEventType(incoming) ? incoming : null;
|
||||
|
||||
@ -1,18 +1,48 @@
|
||||
// /src/lib/useGameBannerStore.ts
|
||||
'use client'
|
||||
import { create } from 'zustand'
|
||||
|
||||
type BannerState = {
|
||||
export type GameBannerData = {
|
||||
visible: boolean
|
||||
map: { key: string|null, label: string|null, bg: string|null }
|
||||
participants: string[]
|
||||
show: (p: { key: string|null, label: string|null, bg: string|null, participants: string[] }) => void
|
||||
hide: () => void
|
||||
variant: 'connected' | 'disconnected'
|
||||
serverLabel?: string
|
||||
mapKey?: string
|
||||
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) => ({
|
||||
visible: false,
|
||||
map: { key: null, label: null, bg: null },
|
||||
participants: [],
|
||||
show: ({ key, label, bg, participants }) => set({ visible: true, map: { key, label, bg }, participants }),
|
||||
hide: () => set({ visible: false, participants: [], map: { key: null, label: null, bg: null } }),
|
||||
type GameBannerState = {
|
||||
gameBanner: GameBannerData | null
|
||||
gameBannerPx: number
|
||||
setGameBanner: (data: GameBannerData | null) => void
|
||||
patchGameBanner: (patch: Partial<GameBannerData>) => void
|
||||
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